Skip to content

RustifyAuth Wiki

Mehrnoush edited this page Oct 25, 2024 · 10 revisions

Welcome to the RustifyAuth Wiki!

This project is an authentication and authorization server library based on OAuth 2.0 and OAuth 2.1, written in Rust. The goal is to provide developers with an extensible, secure, and scalable solution for managing OAuth flows with advanced security features.

Overview

RustifyAuth is a modular OAuth 2.0 and OAuth 2.1-compliant authentication library that enables developers to implement authorization flows such as Authorization Code with PKCE, Client Credentials, Refresh Token, and more. The library is designed for production environments with a focus on performance and security.


Getting Started

Prerequisites

  • Rust (latest stable version)
  • Cargo (Rust's package manager)
  • OpenSSL (for handling encryption)

You can install Rust from the official website: Install Rust.

Installation

To add RustifyAuth to your project, include it in your Cargo.toml file:

[dependencies]
rustify-auth = "0.1"

Then, run cargo build to install the necessary dependencies.

Setting Up the Project After installation, you can start by configuring your environment, token store, and setting up routes. Refer to the Configuration section for more details.


Features

OAuth 2.0 & OAuth 2.1 Support: Full compliance with RFC 6749 (OAuth 2.0) and draft specifications of OAuth 2.1. PKCE Support: Proof Key for Code Exchange, providing enhanced security for public clients. Token Management: In-memory and pluggable token stores for managing access and refresh tokens. Rate Limiting: Protect endpoints from abuse. Advanced Security: Quantum-resistant signing algorithms (Dilithium, Falcon), MFA support, and CSRF protection. JWT Handling: JSON Web Token (JWT) generation, signing, and verification. Pluggable Architecture: Easily replace storage backends or extend with new features. Actix Web Integration: Built-in integration with Actix Web for easy route management. Supported Grant Types RustifyAuth supports multiple grant types to comply with OAuth 2.0/2.1 specifications. The currently supported grant types include:


Authorization Code with PKCE Client Credentials Refresh Token Device Flow Extension Grants In the future, we plan to add additional flows based on community needs.


Configuration

Environment Variables RustifyAuth uses a .env file to handle sensitive data like secrets and tokens. Example .env file:

JWT_SECRET=mysecret
TOKEN_EXPIRATION=3600
DATABASE_URL=postgres://user:password@localhost/dbname

Token Store Configuration By default, RustifyAuth uses an in-memory token store. For production, it is recommended to use a persistent store (e.g., Redis, PostgreSQL). Here's an example of setting up an in-memory token store:

let token_store = InMemoryTokenStore::new();

JWT Configuration Ensure you configure your JWT with appropriate signing algorithms and keys. You can use RSA/ECC or even quantum-resistant algorithms such as Dilithium or Falcon for advanced use cases.


Endpoints

Authorization Endpoint (Authorization Code Flow)

Route: /authorize Method: GET Description: Initiates the OAuth Authorization Code flow. Token Endpoint

Route: /token Method: POST Description: Exchanges authorization codes for access or refresh tokens. Revocation Endpoint

Route: /revoke Method: POST Description: Revokes tokens (both access and refresh tokens). Introspection Endpoint

Route: /introspect Method: POST Description: Validates access tokens. Refer to the full API Documentation for more details on endpoint usage.


Usage

Here’s a quick example to set up an OAuth2 Authorization Code Flow with PKCE:

use rustify_auth::token_store::InMemoryTokenStore;
use rustify_auth::endpoints::{authorize, token};

fn main() {
    let token_store = InMemoryTokenStore::new();

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(token_store.clone()))
            .route("/authorize", web::get().to(authorize))
            .route("/token", web::post().to(token))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await;
}

For more detailed examples and advanced usage, visit the Examples page.


Testing

RustifyAuth comes with a comprehensive suite of tests to ensure the functionality of various components.

Running Tests To run the tests, use the following command:

cargo test



Device Authorization Flow

Overview

The Device Authorization Flow allows devices (e.g., smart TVs or IoT devices) to request access to a user’s account without requiring the user to enter credentials directly on the device. Instead, users are provided with a user_code that they enter on a verification page using a separate device, such as a mobile phone or computer.

This guide walks you through the entire device authorization process, including the steps required to request a device_code, how users can authorize the device, and how the client application can obtain an access token after the authorization is complete.

Steps in the Flow

The flow consists of the following key steps:

  1. Device Requests Authorization:
  • The device sends a POST request to the /device_authorize endpoint with the client ID and requested scopes (if applicable). This request initiates the authorization process and returns a device_code and user_code.
  1. User Authorizes the Device:

The user is prompted to visit a verification page (e.g., https://yourdomain.com/device) and enter the provided user_code to authorize the device. The server marks the corresponding device code as authorized.

  1. Device Polls for Token:

The device periodically sends a request to the /device_token endpoint with the device_code to check if the user has authorized the device. Once authorized, the server responds with an access token.

Endpoints

1. Device Authorization Endpoint (/device_authorize)

This endpoint is used by devices to initiate the authorization process and request a device_code and user_code.

  • URL: /device_authorize

  • Method: POST

  • Request Body:

{
  "client_id": "your_client_id",
  "scope": "read write"
}

Response:

{
  "device_code": "generated_device_code",
  "user_code": "generated_user_code",
  "verification_uri": "https://yourdomain.com/device",
  "expires_in": 600,
  "interval": 5
}
  • Explanation:

device_code: A unique code for the device to use when polling for the token. user_code: The code that the user will enter on the verification page. verification_uri: The URL where the user needs to enter the user_code. expires_in: The time (in seconds) after which the device_code expires. interval: The interval (in seconds) that the device should wait between polling requests.

2. Verification Page

The verification page is where the user enters the user_code to authorize the device. After the user enters the user_code, the server marks the corresponding device_code as authorized.

  • URL: https://yourdomain.com/device

  • User Input: User enters the user_code received from the device.

  • Backend Process: The server checks the user_code, and if valid, authorizes the device code in the system.

3. Device Token Polling Endpoint (/device_token)

After the device has obtained a device_code, it periodically sends a request to the /device_token endpoint to check if the user has authorized the device.

  • URL: /device_token

  • Method: POST

  • Request Body:

{
  "client_id": "your_client_id",
  "device_code": "your_device_code"
}
  • Response (Before Authorization):
{
  "error": "authorization_pending"
}
  • Response (After Authorization):
{
  "access_token": "generated_access_token",
  "token_type": "Bearer",
  "expires_in": 3600
}

*** Explanation**:

  • access_token: The token that can be used to access protected resources.
  • token_type: The type of token (usually Bearer).
  • expires_in: The duration (in seconds) that the token is valid for.

*** Error Responses:**

  • authorization_pending: The user has not yet authorized the device.
  • expired_token: The device_code has expired.
  • invalid_request: The request contains an invalid or non-existent device_code.

Example Flow

1. Device Requests Authorization

The device sends a request to /device_authorize to initiate the authorization process. The server responds with a device_code and user_code.

POST /device_authorize
{
  "client_id": "your_client_id",
  "scope": "read write"
}

Response:
{
  "device_code": "device_code_123",
  "user_code": "user_code_456",
  "verification_uri": "https://yourdomain.com/device",
  "expires_in": 600,
  "interval": 5
}

2. User Authorizes the Device

The user visits https://yourdomain.com/device on a separate device (e.g., their phone) and enters the user_code to authorize the device.

3. Device Polls for Token

The device periodically sends a request to /device_token with the device_code. If the user has not yet authorized the device, the response will contain the error authorization_pending.

POST /device_token
{
  "client_id": "your_client_id",
  "device_code": "device_code_123"
}

Response (Before Authorization):
{
  "error": "authorization_pending"
}

Once the user authorizes the device, the device receives the access token:

Response (After Authorization):
{
  "access_token": "access_token_789",
  "token_type": "Bearer",
  "expires_in": 3600
}

4. Accessing Protected Resources

Now that the device has an access_token, it can use this token to access protected resources by sending it in the Authorization header.

GET /protected_resource
Authorization: Bearer access_token_789

Configuration

Environment Variables

You can configure the following environment variables to customize the behavior of the Device Authorization Flow:

  • VERIFICATION_URI: The URL where users enter the user_code. (Default: https://yourdomain.com/device)
  • EXPIRES_IN: The expiration time (in seconds) for device codes. (Default: 600)
  • INTERVAL: The interval (in seconds) that the device should wait between polling for the token. (Default: 5)

Example .env File

VERIFICATION_URI=https://yourdomain.com/device
EXPIRES_IN=600
INTERVAL=5

Error Handling

The following errors might occur during the device authorization flow:

  • authorization_pending: The user has not yet authorized the device.
  • expired_token: The device_code has expired.
  • invalid_request: The request contains an invalid or non-existent device_code.

Security Considerations

  1. Rate Limiting: Ensure that the /device_token polling endpoint is rate-limited to prevent abuse.
  2. Authorization Time: Devices should respect the expires_in and interval values provided in the DeviceAuthorizationResponse.
  3. Scopes and Permissions: Ensure that tokens are generated with the correct scopes based on the client's request.


Authorization Grant Type Flow

This section provides a comprehensive guide to implementing the Authorization Code Flow with PKCE (Proof Key for Code Exchange) in RustifyAuth, ensuring a secure and robust authorization process for developers.

Overview

The Authorization Code Flow with PKCE in RustifyAuth is designed to enable secure access delegation, commonly used by both confidential and public clients. PKCE adds an extra layer of security to ensure that authorization codes cannot be intercepted or misused.

This guide walks through each step of the Authorization Grant Type, allowing developers to securely implement the flow in their applications.

Step-by-Step Guide

Authorization Request

  1. Initiate Authorization The client (application) begins the authorization request by providing the following parameters to the authorization endpoint:
  • response_type: Must be set to "code" for authorization code flow.
  • client_id: The unique identifier of the client application.
  • redirect_uri: The URI where the authorization code will be sent after the user authorizes access.
  • scope: Optional, specifies the level of access requested (e.g., "read write").
  • state: Optional, recommended for preventing CSRF attacks.
  • code_challenge and code_challenge_method: PKCE parameters, required for public clients to enhance security.
  1. Endpoint Example
GET /authorize?response_type=code&client_id=your_client_id&redirect_uri=https://yourapp.com/callback&scope=read&state=xyz&code_challenge=abcd123&code_challenge_method=S256

PKCE Challenge The PKCE mechanism requires generating a code challenge and code verifier.

  1. Generate Code Challenge and Verifier
  • Verifier: Random string, 43-128 characters.
  • Challenge: SHA-256 hash of the verifier (Base64URL encoded).
  1. Supported Methods
  • S256: Recommended for security.
  • plain: Supported, but less secure.

Authorization Code Generation Upon receiving the authorization request, RustifyAuth performs the following:

  1. Validate Client and PKCE: Ensures client_id is valid, redirect_uri is authorized, and PKCE parameters are correct.
  2. Generate Authorization Code: A unique, time-limited code is generated.
  3. Redirect to Client: RustifyAuth redirects the user back to the specified redirect_uri with the authorization code.

Token Exchange To obtain an access token:

  1. Token Request The client sends the authorization code, client_id, redirect_uri, and code_verifier (PKCE verifier) to the token endpoint.
POST /token
{
  "client_id": "your_client_id",
  "code": "authorization_code",
  "redirect_uri": "https://yourapp.com/callback",
  "code_verifier": "code_verifier_string"
}
  1. Token Validation and Exchange

RustifyAuth validates the code, PKCE verifier, and expiration. Upon success, it returns an access token and refresh token to the client.

Token Response Structure The token response contains the following attributes:

  • access_token: The token used to access resources.
  • refresh_token: The token used to obtain a new access token when the current one expires.
  • token_type: Usually "Bearer".
  • expires_in: Time in seconds for the token’s validity.
  • scope: The granted scope(s).

Error Handling RustifyAuth handles various error cases in the Authorization Grant Flow. Below are common errors and suggested handling:

  • Invalid Code: Returned if the provided authorization code is invalid.
  • Expired Code: Returned if the authorization code has expired.
  • Invalid PKCE: Returned if the PKCE challenge validation fails.
  • Token Generation Error: Occurs if there’s an issue generating tokens.

Security Recommendations To ensure a secure implementation:

  • Use PKCE: Especially for public clients to prevent authorization code interception.
  • Scope Validation: Ensure scopes are checked against authorized scopes.
  • Session Management: Use secure session cookies or tokens with HttpOnly and Secure flags.
  • Rate Limiting: Limit repeated authorization attempts to prevent brute-force attacks.

Example Code Below is sample code for implementing the Authorization Grant Flow using RustifyAuth.

Authorization Request:

let auth_request = AuthorizationRequest {
    response_type: "code".to_string(),
    client_id: "your_client_id".to_string(),
    redirect_uri: "https://yourapp.com/callback".to_string(),
    scope: Some("read write".to_string()),
    state: Some("xyz".to_string()),
    code_challenge: Some("base64url_encoded_sha256_challenge".to_string()),
    code_challenge_method: Some("S256".to_string()),
};

Token Exchange:

let token_request = TokenExchangeRequest {
    client_id: "your_client_id".to_string(),
    code: "authorization_code".to_string(),
    redirect_uri: "https://yourapp.com/callback".to_string(),
    code_verifier: "original_code_verifier".to_string(),
};
let token_response = rustify_auth.exchange_code_for_token(token_request);

Advanced Use Cases

  1. TOTP Authentication: For additional security, combine the Authorization Code Flow with TOTP for multifactor authentication.
  2. Introspection and Revocation: Enable introspection and revocation endpoints to manage and verify token validity.


Client Credentials Grant

This section provides a clear explanation of the Client Credentials Flow, enabling developers to implement machine-to-machine authorization for secure API access.

Overview

The Client Credentials Grant in RustifyAuth is designed for scenarios where applications need to directly access their resources without user involvement. This grant type is best suited for machine-to-machine authentication, where secure access to APIs is essential.

RustifyAuth’s Client Credentials Flow verifies client credentials, scopes, and generates a JWT or opaque token for secure access to APIs.

Step-by-Step Guide

Client Credentials Request

  1. Initiate the Client Credentials Grant The client application submits a request containing the following parameters to the /token endpoint:

grant_type: Must be set to "client_credentials" for this flow. client_id: The unique identifier for the client application. client_secret: A secret known only to the client and the authorization server. scope: Optional, specifies the level of access requested.

  1. Endpoint Example
POST /token
{
  "grant_type": "client_credentials",
  "client_id": "your_client_id",
  "client_secret": "your_client_secret",
  "scope": "read write"
}

Token Issuance Upon receiving a valid request, RustifyAuth performs the following:

  1. Validate Client Credentials RustifyAuth verifies client_id and client_secret by checking against stored credentials. If credentials are valid, RustifyAuth proceeds to issue a token.

  2. Validate Scope RustifyAuth checks if the requested scope is authorized for the client. If an invalid scope is provided, an error is returned.

  3. Generate Access Token RustifyAuth generates an access token for the client, typically with a set expiration time (e.g., 1 hour).

  4. Return Token Response A JSON response is sent to the client, containing the access token and token details.

Token Response Structure The token response contains the following attributes:

  • access_token: The token used to access APIs.
  • token_type: Typically "Bearer".
  • expires_in: Time in seconds for which the token is valid.
  • scope: The granted scope(s).

Example Response:

{
  "access_token": "your_access_token",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "read write"
}

Error Handling RustifyAuth provides informative errors to help identify issues with the client credentials request. Here are common errors:

  • Invalid Client: Returned when client_id or client_secret is incorrect.
  • Unsupported Grant Type: Occurs if the grant_type is not "client_credentials".
  • Invalid Scope: Triggered if the requested scope is not permitted for the client.

Security Recommendations To maximize security in your Client Credentials Grant implementation, consider the following:

  • Secure Storage for Client Secrets: Store client_secret securely (e.g., in a secure vault).
  • Use HTTPS: Always send client credentials over HTTPS to prevent interception.
  • Scope Restriction: Only assign the necessary scopes for each client, minimizing permissions.
  • Rate Limiting: Implement rate limiting to prevent abuse by malicious clients.

Example Code Below is sample code to demonstrate the Client Credentials Flow using RustifyAuth.

Client Credentials Request:

let client_credentials_request = ClientCredentialsRequest {
    grant_type: "client_credentials".to_string(),
    client_id: "your_client_id".to_string(),
    client_secret: "your_client_secret".to_string(),
    scope: Some("read write".to_string()),
};

Token Issuance:

let storage = Arc::new(MockStorageBackend::new()); // Mock or actual storage backend
let token_response = handle_client_credentials(
    web::Json(client_credentials_request),
    web::Data::new(storage),
).await;

Advanced Use Cases

  1. Scope Restrictions: Tailor scopes for specific clients to limit API access.
  2. Expiring Tokens: Set custom expiration durations for tokens, adapting to each client’s needs.
  3. Custom Token Claims: Embed additional claims in tokens, such as client information, to enhance authorization checks.


Extension Grant

This section explains how to leverage extension grants, including custom and device code grants, for additional authorization flows beyond standard OAuth2 grants.

Overview

The Extension Grant feature in RustifyAuth supports custom authorization requirements beyond standard flows. This guide includes instructions for:

  • Device Code Grant: Common in applications with limited input devices, like IoT devices or TVs.
  • Custom Grant Types: Enables developers to define unique grant types for specific requirements.

Device Code Grant Flow

The Device Code Grant Flow enables users to authorize devices through a web or mobile interface, where the device itself has limited input options.

Generating a Device Code

  1. Initiate Device Code Request A request is sent to RustifyAuth to generate a device code for authorization, which responds with:
  • device_code: Unique code to identify the device.
  • user_code: Code displayed for the user to input on the authorization page.
  • verification_uri: URL for the user to input the code.
  • expires_in: Validity period of the device code.
  • interval: Polling interval for token requests.

Example:

let handler = DefaultDeviceFlowHandler::new("https://example.com");
let device_code_response = handler.generate_device_code();
  1. Device Code Response
{
  "device_code": "generated_device_code",
  "user_code": "1234-5678",
  "verification_uri": "https://example.com/device",
  "expires_in": 600,
  "interval": 5
}

Polling for Token After the user authorizes, the device can begin polling for the access token:

  1. Poll Device Code The device periodically sends the device_code to RustifyAuth to check if authorization is complete.

  2. Token Issuance Once authorized, RustifyAuth returns an access token with an expiration time and scope.

Example Polling:

let result = handler.poll_device_code("valid_device_code");

Successful Token Response:

{
  "access_token": "device_access_token",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "device_refresh_token",
  "scope": "read write"
}

Error Handling for Device Code

  • Invalid Grant: Sent if the device_code is invalid or expired.
  • Pending Authorization: Returned when authorization is still pending.

Custom Extension Grant Flow

The custom grant flow allows you to define unique grant types using the ExtensionGrantHandler interface.

Handling Custom Grant Requests

  1. Implement Custom Grant Handler Define your custom grant requirements by implementing ExtensionGrantHandler. RustifyAuth validates the grant_type and parameters, allowing you to tailor the flow as needed.

  2. Custom Grant Token Issuance The handler returns a TokenResponse similar to other grants but can include custom scopes and expiration.

Example Code:

let handler = CustomGrant;
let mut params = HashMap::new();
params.insert("custom_param".to_string(), "value".to_string());

let result = handler.handle_extension_grant("urn:ietf:params:oauth:grant-type:custom-grant", &params);

Response Example

{
  "access_token": "custom_access_token",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "custom_refresh_token",
  "scope": "custom_scope"
}

Error Handling for Custom Grants

  • Unsupported Grant Type: If an unsupported grant_type is provided, an error is returned.
  • Invalid Scope: Ensures requested scopes match the client's allowed permissions.

Example Code Below is sample code for implementing a Device Code and Custom Grant with RustifyAuth.

Device Code Generation Example:

let handler = DefaultDeviceFlowHandler::new("https://example.com");
let device_code_response = handler.generate_device_code();

Device Code Polling Example:

let result = handler.poll_device_code("valid_device_code");
if let Ok(token_response) = result {
    println!("Access Token: {}", token_response.access_token);
}

Custom Grant Handler Example:

let handler = CustomGrant;
let mut params = HashMap::new();
params.insert("custom_param".to_string(), "value".to_string());

let result = handler.handle_extension_grant("urn:ietf:params:oauth:grant-type:custom-grant", &params);
if let Ok(token_response) = result {
    println!("Access Token: {}", token_response.access_token);
}



Dynamic Client Registration

Overview

Dynamic Client Registration allows new clients (applications) to register with the authorization server dynamically, receiving a client ID and secret for future authentication. RustifyAuth supports this functionality, enabling developers to automate the registration of new applications.

Client Registration Endpoint

The /register endpoint enables clients to register themselves with the authorization server, specifying necessary details such as redirect URIs and supported grant types.

Endpoint: /register Method: POST Headers: Authorization: Bearer <admin_token> - Required for authorization. X-Token-Binding (optional) - A unique identifier for binding the client registration to a specific security token. Request Payload The payload should contain essential metadata about the client:

{
  "client_name": "My Application",
  "redirect_uris": ["https://myapp.com/callback"],
  "grant_types": ["authorization_code"],
  "response_types": ["code"],
  "software_statement": "optional_statement"
}
  • client_name: The name of the client application.
  • redirect_uris: A list of URIs to which authorization responses can be sent. At least one URI is required.
  • grant_types: Grant types supported by the client (e.g., authorization_code, client_credentials).
  • response_types: The expected response types (e.g., code, token).
  • software_statement: Optional information about the software, such as metadata or version.

Sample Registration Request

curl -X POST https://yourauthserver.com/register \
  -H "Authorization: Bearer valid_admin_token" \
  -H "Content-Type: application/json" \
  -d '{
        "client_name": "Example Client",
        "redirect_uris": ["https://example.com/callback"],
        "grant_types": ["authorization_code"],
        "response_types": ["code"]
      }'

Response On successful registration, the server returns the following:

{
  "client_id": "client_<unique_identifier>",
  "client_secret": "secret_<unique_identifier>"
}
  • client_id: The unique identifier for the registered client.
  • client_secret: The client’s secret, used for authentication.

Updating Client Information

The /update/{client_id} endpoint allows an existing client’s information to be updated, such as redirect URIs, grant types, or client name. Only authorized administrators may update client information.

Endpoint: /update/{client_id} Method: PUT Headers: Authorization: Bearer <admin_token> - Required for authorization. Path Parameter: client_id - The ID of the client to be updated. Request Payload Only the fields to be updated need to be included. Missing fields remain unchanged.

{
  "client_name": "Updated Client Name",
  "redirect_uris": ["https://updatedapp.com/callback"],
  "grant_types": ["client_credentials"],
  "response_types": ["token"]
}

Response If successful, the server returns:

{
  "message": "Client updated successfully"
}

Access Control RustifyAuth integrates Role-Based Access Control (RBAC) for client management endpoints, where only admin-role users can register or update client information. You can customize RBAC to allow different roles or enhance role validation for various grant types and client access levels.

Example RBAC Logic (for testing) The current implementation mock checks for an admin token:

pub fn rbac_check(token: &str, required_role: &str) -> Result<(), &'static str> {
    if token == "valid_admin_token" && required_role == "admin" {
        Ok(())
    } else {
        Err("Unauthorized")
    }
}

Error Handling RustifyAuth returns clear error messages for common issues during registration:

401 Unauthorized: Invalid or missing authorization token. 400 Bad Request: Missing required fields, such as redirect_uris. 404 Not Found: Attempting to update a non-existent client. 500 Internal Server Error: Issues with server configuration or storage. Testing the Registration Process Tests validate registration, updating, and error responses:

Success: Registers a client and checks for a non-empty client_id and client_secret. Unauthorized: Attempts registration with an invalid token, expecting a 401 Unauthorized. Missing Fields: Sends an incomplete payload to verify 400 Bad Request. Update Client: Updates specific fields and verifies persistence in the ClientStore. Sample Test for Registration Success

#[actix_web::test]
async fn test_register_client_success() {
    let store = web::Data::new(RwLock::new(ClientStore::new(InMemoryTokenStore::new())));

    let metadata = ClientMetadata {
        client_name: "Test Client".to_string(),
        redirect_uris: vec!["http://localhost/callback".to_string()],
        grant_types: vec!["authorization_code".to_string()],
        response_types: vec!["code".to_string()],
        software_statement: None,
    };

    let app = test::init_service(App::new().app_data(store.clone()).route(
        "/register",
        web::post().to(register_client_handler::<InMemoryTokenStore>),
    ))
    .await;

    let req = test::TestRequest::post()
        .uri("/register")
        .insert_header(("Authorization", "Bearer valid_admin_token"))
        .set_json(&metadata)
        .to_request();

    let resp: ClientRegistrationResponse = test::call_and_read_body_json(&app, req).await;

    assert!(!resp.client_id.is_empty());
    assert!(!resp.client_secret.is_empty());
}