update: updating files with detailed comments

This commit is contained in:
Patrick Stewart 2024-09-06 11:52:59 -07:00
parent e7f8083b25
commit 2cb685578b
12 changed files with 2153 additions and 238 deletions

View file

@ -7,9 +7,10 @@
* file that was distributed with this source code.
*/
library;
import 'package:protevus_auth/auth.dart';
import 'package:protevus_hashing/hashing.dart';
import 'package:crypto/crypto.dart';
export 'src/auth.dart';
export 'src/auth_code_controller.dart';
export 'src/auth_controller.dart';
export 'src/auth_redirect_controller.dart';
@ -20,3 +21,60 @@ export 'src/exceptions.dart';
export 'src/objects.dart';
export 'src/protocols.dart';
export 'src/validator.dart';
/// A utility method to generate a password hash using the PBKDF2 scheme.
///
/// This function takes a password and salt as input and generates a secure hash
/// using the PBKDF2 (Password-Based Key Derivation Function 2) algorithm.
String generatePasswordHash(
String password,
String salt, {
int hashRounds = 1000,
int hashLength = 32,
Hash? hashFunction,
}) {
final generator = PBKDF2(hashAlgorithm: hashFunction ?? sha256);
return generator.generateBase64Key(password, salt, hashRounds, hashLength);
}
/// A utility method to generate a random base64 salt.
///
/// This function generates a random salt encoded as a base64 string.
/// The salt is useful for adding randomness to password hashing processes,
/// making them more resistant to attacks.
String generateRandomSalt({int hashLength = 32}) {
return generateAsBase64String(hashLength);
}
/// A utility method to generate a ClientID and Client Secret Pair.
///
/// This function creates an [AuthClient] instance, which can be either public or confidential,
/// depending on whether a secret is provided.
///
/// Any client that allows the authorization code flow must include [redirectURI].
///
/// Note that [secret] is hashed with a randomly generated salt, and therefore cannot be retrieved
/// later. The plain-text secret must be stored securely elsewhere.
AuthClient generateAPICredentialPair(
String clientID,
String? secret, {
String? redirectURI,
int hashLength = 32,
int hashRounds = 1000,
Hash? hashFunction,
}) {
if (secret == null) {
return AuthClient.public(clientID, redirectURI: redirectURI);
}
final salt = generateRandomSalt(hashLength: hashLength);
final hashed = generatePasswordHash(
secret,
salt,
hashRounds: hashRounds,
hashLength: hashLength,
hashFunction: hashFunction,
);
return AuthClient.withRedirectURI(clientID, hashed, salt, redirectURI);
}

View file

@ -1,70 +0,0 @@
import 'package:protevus_auth/auth.dart';
import 'package:protevus_hashing/hashing.dart';
import 'package:crypto/crypto.dart';
export 'auth_code_controller.dart';
export 'auth_controller.dart';
export 'auth_redirect_controller.dart';
export 'authorization_parser.dart';
export 'authorization_server.dart';
export 'authorizer.dart';
export 'exceptions.dart';
export 'objects.dart';
export 'protocols.dart';
export 'validator.dart';
/// A utility method to generate a password hash using the PBKDF2 scheme.
///
///
String generatePasswordHash(
String password,
String salt, {
int hashRounds = 1000,
int hashLength = 32,
Hash? hashFunction,
}) {
final generator = PBKDF2(hashAlgorithm: hashFunction ?? sha256);
return generator.generateBase64Key(password, salt, hashRounds, hashLength);
}
/// A utility method to generate a random base64 salt.
///
///
String generateRandomSalt({int hashLength = 32}) {
return generateAsBase64String(hashLength);
}
/// A utility method to generate a ClientID and Client Secret Pair.
///
/// [secret] may be null. If secret is null, the return value is a 'public' client. Otherwise, the
/// client is 'confidential'. Public clients must not include a client secret when sent to the
/// authorization server. Confidential clients must include the secret in all requests. Use public clients when
/// the source code of the client application is visible, i.e. a JavaScript browser application.
///
/// Any client that allows the authorization code flow must include [redirectURI].
///
/// Note that [secret] is hashed with a randomly generated salt, and therefore cannot be retrieved
/// later. The plain-text secret must be stored securely elsewhere.
AuthClient generateAPICredentialPair(
String clientID,
String? secret, {
String? redirectURI,
int hashLength = 32,
int hashRounds = 1000,
Hash? hashFunction,
}) {
if (secret == null) {
return AuthClient.public(clientID, redirectURI: redirectURI);
}
final salt = generateRandomSalt(hashLength: hashLength);
final hashed = generatePasswordHash(
secret,
salt,
hashRounds: hashRounds,
hashLength: hashLength,
hashFunction: hashFunction,
);
return AuthClient.withRedirectURI(clientID, hashed, salt, redirectURI);
}

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:io';
@ -7,13 +16,20 @@ import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart';
/// Provides [AuthCodeController] with application-specific behavior.
///
/// This abstract class defines the interface for a delegate that can be used
/// with [AuthCodeController] to customize the rendering of the login form.
/// It is deprecated along with [AuthCodeController], and developers are
/// advised to see the documentation for alternative approaches.
///
/// The main responsibility of this delegate is to generate an HTML
/// representation of a login form when requested by the [AuthCodeController].
@Deprecated('AuthCodeController is deprecated. See docs.')
abstract class AuthCodeControllerDelegate {
/// Returns an HTML representation of a login form.
///
/// Invoked when [AuthCodeController.getAuthorizationPage] is called in response to a GET request.
/// Must provide HTML that will be returned to the browser for rendering. This form submission of this page
/// should be a POST to [requestUri].
/// This method is responsible for generating the HTML content of a login form
/// that will be displayed to the user when they attempt to authenticate.
///
/// The form submission should include the values of [responseType], [clientID], [state], [scope]
/// as well as user-entered username and password in `x-www-form-urlencoded` data, e.g.
@ -37,24 +53,29 @@ abstract class AuthCodeControllerDelegate {
/// Controller for issuing OAuth 2.0 authorization codes.
///
/// Deprecated, use [AuthRedirectController] instead.
/// This controller handles the authorization code grant flow of OAuth 2.0. It provides
/// endpoints for both initiating the flow (GET request) and completing it (POST request).
///
/// This controller provides an endpoint for the creating an OAuth 2.0 authorization code. This authorization code
/// can be exchanged for an access token with an [AuthController]. This is known as the OAuth 2.0 'Authorization Code Grant' flow.
///
/// See operation methods [getAuthorizationPage] and [authorize] for more details.
///
/// Usage:
///
/// router
/// .route("/auth/code")
/// .link(() => new AuthCodeController(authServer));
///
@Deprecated('Use AuthRedirectController instead.')
class AuthCodeController extends ResourceController {
/// Creates a new instance of an [AuthCodeController].
///
/// [authServer] is the required authorization server. If [delegate] is provided, this controller will return a login page for all GET requests.
/// This constructor initializes an [AuthCodeController] with the provided [authServer].
/// It is marked as deprecated, and users are advised to use [AuthRedirectController] instead.
///
/// Parameters:
/// - [authServer]: The required authorization server used for handling authentication.
/// - [delegate]: An optional [AuthCodeControllerDelegate] that, if provided, allows this controller
/// to return a login page for all GET requests.
///
/// The constructor also sets the [acceptedContentTypes] to only accept
/// "application/x-www-form-urlencoded" content type.
///
/// This controller is part of the OAuth 2.0 authorization code flow and is used
/// for issuing authorization codes. However, due to its deprecated status,
/// it's recommended to transition to newer alternatives as specified in the documentation.
@Deprecated('Use AuthRedirectController instead.')
AuthCodeController(this.authServer, {this.delegate}) {
acceptedContentTypes = [
@ -63,6 +84,11 @@ class AuthCodeController extends ResourceController {
}
/// A reference to the [AuthServer] used to grant authorization codes.
///
/// This [AuthServer] instance is responsible for handling the authentication
/// and authorization processes, including the generation and validation of
/// authorization codes. It is a crucial component of the OAuth 2.0 flow
/// implemented by this controller.
final AuthServer authServer;
/// A randomly generated value the client can use to verify the origin of the redirect.
@ -70,23 +96,53 @@ class AuthCodeController extends ResourceController {
/// Clients must include this query parameter and verify that any redirects from this
/// server have the same value for 'state' as passed in. This value is usually a randomly generated
/// session identifier.
///
/// This property is bound to the 'state' query parameter in the request URL.
/// It plays a crucial role in preventing cross-site request forgery (CSRF) attacks
/// by ensuring that the authorization request and response originate from the same client session.
///
/// The 'state' parameter should be:
/// - Unique for each authorization request
/// - Securely generated to be unguessable
/// - Stored by the client for later comparison
///
/// When the authorization server redirects the user back to the client,
/// it includes this state value, allowing the client to verify that the redirect
/// is in response to its own authorization request.
@Bind.query("state")
String? state;
/// The type of response expected from the authorization server.
///
/// This parameter is bound to the 'response_type' query parameter in the request URL.
/// For the authorization code flow, this value must be 'code'.
///
/// The response type indicates to the authorization server which grant type
/// is being utilized. In this case, 'code' signifies that the client expects
/// to receive an authorization code that can be exchanged for an access token
/// in a subsequent request.
///
/// Must be 'code'.
@Bind.query("response_type")
String? responseType;
/// The client ID of the authenticating client.
///
/// This must be a valid client ID according to [authServer].\
/// This property is bound to the 'client_id' query parameter in the request URL.
/// It represents the unique identifier of the client application requesting authorization.
///
/// The client ID must be registered and valid according to the [authServer].
/// It is used to identify the client during the OAuth 2.0 authorization process.
///
/// This field is nullable, but typically required for most OAuth 2.0 flows.
/// If not provided or invalid, the authorization request may be rejected.
@Bind.query("client_id")
String? clientID;
/// Renders an HTML login form.
final AuthCodeControllerDelegate? delegate;
/// Returns an HTML login form.
/// Returns an HTML login form for OAuth 2.0 authorization.
///
/// A client that wishes to authenticate with this server should direct the user
/// to this page. The user will enter their username and password that is sent as a POST
@ -115,20 +171,61 @@ class AuthCodeController extends ResourceController {
/// Creates a one-time use authorization code.
///
/// This method will respond with a redirect that contains an authorization code ('code')
/// and the passed in 'state'. If this request fails, the redirect URL
/// will contain an 'error' key instead of the authorization code.
/// This method handles the POST request for the OAuth 2.0 authorization code grant flow.
/// It authenticates the user with the provided credentials and, if successful, generates
/// a one-time use authorization code.
///
/// This method is typically invoked by the login form returned from the GET to this controller.
@Operation.post()
Future<Response> authorize({
/// The username of the authenticating user.
///
/// This parameter is bound to the 'username' query parameter in the request URL.
/// It represents the username of the user attempting to authenticate.
///
/// The username is used in conjunction with the password to verify the user's identity
/// during the OAuth 2.0 authorization code grant flow. It is a crucial part of the
/// user authentication process.
///
/// This field is nullable, but typically required for successful authentication.
/// If not provided or invalid, the authorization request may be rejected.
@Bind.query("username") String? username,
/// The password of the authenticating user.
///
/// This parameter is bound to the 'password' query parameter in the request URL.
/// It represents the password of the user attempting to authenticate.
///
/// The password is used in conjunction with the username to verify the user's identity
/// during the OAuth 2.0 authorization code grant flow. It is a crucial part of the
/// user authentication process.
///
/// This field is nullable, but typically required for successful authentication.
/// If not provided or invalid, the authorization request may be rejected.
///
/// Note: Transmitting passwords as query parameters is not recommended for production
/// environments due to security concerns. This approach should only be used in
/// controlled, secure environments or for testing purposes.
@Bind.query("password") String? password,
/// A space-delimited list of access scopes being requested.
///
/// This parameter is bound to the 'scope' query parameter in the request URL.
/// It represents the permissions that the client is requesting access to.
///
/// The scope is typically a string containing one or more space-separated
/// scope values. Each scope value represents a specific permission or
/// set of permissions that the client is requesting.
///
/// For example, a scope might look like: "read_profile edit_profile"
///
/// The authorization server can use this information to present
/// the user with a consent screen, allowing them to approve or deny
/// specific permissions requested by the client.
///
/// This field is optional. If not provided, the authorization server
/// may assign a default set of scopes or handle the request according
/// to its own policies.
@Bind.query("scope") String? scope,
}) async {
final client = await authServer.getClient(clientID!);
@ -174,6 +271,25 @@ class AuthCodeController extends ResourceController {
}
}
/// Overrides the default documentation for the request body of this controller's operations.
///
/// This method is called during the OpenAPI documentation generation process.
/// It modifies the request body schema for POST operations to:
/// 1. Set the format of the 'password' field to "password".
/// 2. Mark certain fields as required in the request body.
///
/// The method specifically targets the "application/x-www-form-urlencoded" content type
/// in POST requests. It updates the schema to indicate that the 'password' field should
/// be treated as a password input, and sets the following fields as required:
/// - client_id
/// - state
/// - response_type
/// - username
/// - password
///
/// Returns:
/// An [APIRequestBody] object representing the documented request body,
/// or null if there is no request body for the operation.
@override
APIRequestBody? documentOperationRequestBody(
APIDocumentContext context,
@ -194,6 +310,20 @@ class AuthCodeController extends ResourceController {
return body;
}
/// Overrides the default documentation for operation parameters.
///
/// This method is called during the OpenAPI documentation generation process.
/// It modifies the parameter documentation for the controller's operations by:
/// 1. Retrieving the default parameters using the superclass method.
/// 2. Setting all parameters except 'scope' as required.
///
/// Parameters:
/// context: The [APIDocumentContext] for the current documentation generation.
/// operation: The [Operation] being documented.
///
/// Returns:
/// A List of [APIParameter] objects representing the documented parameters,
/// with updated 'isRequired' properties.
@override
List<APIParameter> documentOperationParameters(
APIDocumentContext context,
@ -206,6 +336,28 @@ class AuthCodeController extends ResourceController {
return params;
}
/// Generates documentation for the operation responses of this controller.
///
/// This method overrides the default behavior to provide custom documentation
/// for the GET and POST operations of the AuthCodeController.
///
/// For GET requests:
/// - Defines a 200 OK response that serves a login form in HTML format.
///
/// For POST requests:
/// - Defines a 302 Found (Moved Temporarily) response for successful requests,
/// indicating that the 'code' query parameter in the redirect URI contains
/// the authorization code, or an 'error' parameter is present for errors.
/// - Defines a 400 Bad Request response for cases where the 'client_id' is
/// invalid and the redirect URI cannot be verified.
///
/// Parameters:
/// context: The API documentation context.
/// operation: The operation being documented.
///
/// Returns:
/// A Map of status codes to APIResponse objects describing the possible
/// responses for the operation.
@override
Map<String, APIResponse> documentOperationResponses(
APIDocumentContext context,
@ -240,6 +392,24 @@ class AuthCodeController extends ResourceController {
throw StateError("AuthCodeController documentation failed.");
}
/// Customizes the documentation for the operations of this controller.
///
/// This method overrides the default implementation to add additional
/// information specific to the OAuth 2.0 authorization code flow.
///
/// It performs the following tasks:
/// 1. Calls the superclass method to get the default operation documentation.
/// 2. Updates the authorization URL in the documented authorization code flow
/// of the auth server to match the current route.
///
/// Parameters:
/// context: The [APIDocumentContext] for the current documentation generation.
/// route: The route string for this controller.
/// path: The [APIPath] object representing the path in the API documentation.
///
/// Returns:
/// A Map of operation names to [APIOperation] objects representing the
/// documented operations for this controller.
@override
Map<String, APIOperation> documentOperations(
APIDocumentContext context,
@ -252,6 +422,27 @@ class AuthCodeController extends ResourceController {
return ops;
}
/// Generates a redirect response for the OAuth 2.0 authorization code flow.
///
/// This method constructs a redirect URI based on the provided parameters and
/// returns a Response object with appropriate headers for redirection.
///
/// Parameters:
/// - [inputUri]: The base URI to redirect to. If null, falls back to the client's redirectURI.
/// - [clientStateOrNull]: The state parameter provided by the client. If not null, it's included in the redirect URI.
/// - [code]: The authorization code to be included in the redirect URI. Optional.
/// - [error]: An AuthServerException containing error details. Optional.
///
/// Returns:
/// - A Response object with status 302 (Found) and appropriate headers for redirection.
/// - If no valid redirect URI can be constructed, returns a 400 (Bad Request) response.
///
/// The method constructs the redirect URI by:
/// 1. Determining the base URI (from input or client's redirect URI)
/// 2. Adding query parameters for code, state, and error as applicable
/// 3. Constructing a new URI with these parameters
///
/// The response includes headers for location, cache control, and pragma.
static Response _redirectResponse(
String? inputUri,
String? clientStateOrNull, {

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:io';
@ -49,6 +58,30 @@ class AuthController extends ResourceController {
final AuthorizationBasicParser _parser = const AuthorizationBasicParser();
/// This class, AuthController, is responsible for handling OAuth 2.0 token operations.
/// It provides functionality for issuing and refreshing access tokens using various grant types.
///
/// Key features:
/// - Supports 'password', 'refresh_token', and 'authorization_code' grant types
/// - Handles client authentication via Basic Authorization header
/// - Processes token requests and returns RFC6749 compliant responses
/// - Includes error handling for various authentication scenarios
/// - Provides OpenAPI documentation support
///
/// The main method, 'grant', processes token requests based on the provided grant type.
/// It interacts with an AuthServer to perform the actual authentication and token generation.
///
/// This controller also includes methods for generating API documentation,
/// including operation parameters, request body, and responses.
///
/// Usage:
/// router
/// .route("/auth/token")
/// .link(() => new AuthController(authServer));
///
/// Note: This controller expects client credentials to be provided in the Authorization header
/// using the Basic authentication scheme.
///
/// Creates or refreshes an authentication token.
///
/// When grant_type is 'password', there must be username and password values.
@ -59,11 +92,88 @@ class AuthController extends ResourceController {
/// include a valid Client ID and Secret in the Basic authorization scheme format.
@Operation.post()
Future<Response> grant({
/// The username of the user attempting to authenticate.
///
/// This parameter is typically used with the 'password' grant type.
/// It should be provided as a query parameter in the request.
@Bind.query("username") String? username,
/// The password of the user attempting to authenticate.
///
/// This parameter is typically used with the 'password' grant type.
/// It should be provided as a query parameter in the request.
/// Note: Sending passwords as query parameters is not recommended for production environments due to security concerns.
@Bind.query("password") String? password,
/// The refresh token used to obtain a new access token.
///
/// This parameter is typically used with the 'refresh_token' grant type.
/// It should be provided as a query parameter in the request.
/// The refresh token is used to request a new access token when the current one has expired.
///
/// Example:
/// curl -X POST -d "grant_type=refresh_token&refresh_token=<refresh_token>" https://example.com/auth/token
///
/// Note: The refresh token should be securely stored and managed by the client application.
/// It is important to handle refresh tokens with care to prevent unauthorized access to user resources.
///
/// See also:
/// - [RFC 6749, Section 6](https://tools.ietf.org/html/rfc6749#section-6) for more details on the refresh token grant type.
/// - [OAuth 2.0 Refresh Token Grant](https://oauth.net/2/grant-types/refresh-token/) for a detailed explanation of the refresh token grant type.
/// - [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-2.1) for security considerations when implementing OAuth 2.0.
@Bind.query("refresh_token") String? refreshToken,
/// The authorization code obtained from the authorization server.
///
/// This parameter is typically used with the 'authorization_code' grant type.
/// It should be provided as a query parameter in the request.
/// The authorization code is used to request an access token after the user has granted permission to the client application.
///
/// Example:
/// curl -X POST -d "grant_type=authorization_code&code=<authorization_code>&redirect_uri=<redirect_uri>" https://example.com/auth/token
///
/// Note: The authorization code should be securely transmitted and used only once to prevent replay attacks.
/// It is important to handle authorization codes with care to protect user data and ensure the security of the OAuth 2.0 flow.
///
/// See also:
/// - [RFC 6749, Section 4.1.3](https://tools.ietf.org/html/rfc6749#section-4.1.3) for more details on the authorization code grant type.
/// - [OAuth 2.0 Authorization Code Grant](https://oauth.net/2/grant-types/authorization-code/) for a detailed explanation of the authorization code grant type.
/// - [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-2.1) for security considerations when implementing OAuth 2.0.
@Bind.query("code") String? authCode,
/// The URI to which the authorization server will redirect the user-agent after obtaining authorization.
///
/// This parameter is typically used with the 'authorization_code' grant type.
/// It should be provided as a query parameter in the request.
/// The redirect URI is used to ensure that the authorization code is sent to the correct client application.
///
/// Example:
/// curl -X POST -d "grant_type=authorization_code&code=<authorization_code>&redirect_uri=https://example.com/callback" https://example.com/auth/token
///
/// Note: The redirect URI should be registered with the authorization server and should match the URI used during the authorization request.
/// It is important to handle redirect URIs with care to prevent unauthorized access to user resources.
///
/// See also:
/// - [RFC 6749, Section 4.1.3](https://tools.ietf.org/html/rfc6749#section-4.1.3) for more details on the authorization code grant type.
/// - [OAuth 2.0 Authorization Code Grant](https://oauth.net/2/grant-types/authorization-code/) for a detailed explanation of the authorization code grant type.
/// - [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-2.1) for security considerations when implementing OAuth 2.0.
@Bind.query("grant_type") String? grantType,
/// The scope of the access request, which defines the resources and permissions that the client application is requesting.
///
/// This parameter is optional and should be provided as a query parameter in the request.
/// The scope value is a space-delimited list of scope identifiers, which indicate the specific resources and permissions that the client application needs to access.
///
/// Example:
/// curl -X POST -d "grant_type=authorization_code&code=<authorization_code>&redirect_uri=<redirect_uri>&scope=read write" https://example.com/auth/token
///
/// Note: The scope parameter should be used to limit the access granted to the client application to only the necessary resources and permissions.
/// It is important to handle scope values with care to ensure that the client application does not have unintended access to user resources.
///
/// See also:
/// - [RFC 6749, Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3) for more details on the scope parameter.
/// - [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-13#section-2.2) for security considerations when implementing OAuth 2.0 scope.
/// - [OAuth 2.0 Scope](https://oauth.net/2/scope/) for a detailed explanation of the scope parameter and its usage.
@Bind.query("scope") String? scope,
}) async {
AuthBasicCredentials basicRecord;
@ -118,6 +228,28 @@ class AuthController extends ResourceController {
/// Transforms a [AuthToken] into a [Response] object with an RFC6749 compliant JSON token
/// as the HTTP response body.
///
/// This static method takes an [AuthToken] object and converts it into a [Response] object
/// that adheres to the OAuth 2.0 specification (RFC6749). The response includes:
/// - A status code of 200 (OK)
/// - Headers to prevent caching of the token
/// - A body containing the token information in JSON format
///
/// Parameters:
/// - token: An [AuthToken] object containing the authentication token details
///
/// Returns:
/// A [Response] object with the token information, ready to be sent to the client
///
/// Example usage:
/// ```dart
/// AuthToken myToken = // ... obtain token
/// Response response = AuthController.tokenResponse(myToken);
/// ```
///
/// See also:
/// - [RFC6749](https://tools.ietf.org/html/rfc6749) for the OAuth 2.0 specification
/// - [AuthToken] for the structure of the token object
static Response tokenResponse(AuthToken token) {
return Response(
HttpStatus.ok,
@ -126,6 +258,24 @@ class AuthController extends ResourceController {
);
}
/// Processes the response before it is sent, specifically handling duplicate parameter errors.
///
/// This method is called just before a response is sent. It checks for responses with a 400 status code
/// and modifies the error message in case of duplicate parameters in the request, which violates the OAuth 2.0 specification.
///
/// The method performs the following actions:
/// 1. Checks if the response status code is 400 (Bad Request).
/// 2. If the response body contains an "error" key with a string value, it examines the error message.
/// 3. If the error message indicates multiple values (likely due to duplicate parameters), it replaces the error message
/// with a standard "invalid_request" error as defined in the OAuth 2.0 specification.
///
/// This post-processing helps to maintain compliance with the OAuth 2.0 specification by providing a standard error
/// response for invalid requests, even in the case of duplicate parameters which are not explicitly handled elsewhere.
///
/// Parameters:
/// response: The Response object that will be sent to the client.
///
/// Note: This method directly modifies the response object if conditions are met.
@override
void willSendResponse(Response response) {
if (response.statusCode == 400) {
@ -145,6 +295,20 @@ class AuthController extends ResourceController {
}
}
/// Modifies the list of API parameters documented for this operation.
///
/// This method overrides the default behavior to remove the 'Authorization' header
/// from the list of documented parameters. This is typically done because the
/// Authorization header is handled separately in OAuth 2.0 flows and doesn't need
/// to be explicitly documented as an operation parameter.
///
/// Parameters:
/// - context: The current API documentation context.
/// - operation: The operation being documented (can be null).
///
/// Returns:
/// A list of [APIParameter] objects representing the documented parameters
/// for this operation, with the Authorization header removed.
@override
List<APIParameter> documentOperationParameters(
APIDocumentContext context,
@ -155,6 +319,21 @@ class AuthController extends ResourceController {
return parameters;
}
/// Customizes the documentation for the request body of this operation.
///
/// This method overrides the default behavior to add specific requirements
/// and formatting for the OAuth 2.0 token endpoint:
///
/// 1. It marks the 'grant_type' parameter as required in the request body.
/// 2. It sets the format of the 'password' field to "password", indicating
/// that it should be treated as a sensitive input in API documentation tools.
///
/// Parameters:
/// - context: The current API documentation context.
/// - operation: The operation being documented (can be null).
///
/// Returns:
/// An [APIRequestBody] object with the customized schema for the request body.
@override
APIRequestBody documentOperationRequestBody(
APIDocumentContext context,
@ -169,6 +348,20 @@ class AuthController extends ResourceController {
return body;
}
/// Customizes the API documentation for the operations handled by this controller.
///
/// This method overrides the default behavior to:
/// 1. Add OAuth 2.0 client authentication security requirement to all operations.
/// 2. Set the token and refresh URLs for the documented authorization code flow.
/// 3. Set the token and refresh URLs for the documented password flow.
///
/// Parameters:
/// - context: The current API documentation context.
/// - route: The route string for the current operations.
/// - path: The APIPath object representing the current path.
///
/// Returns:
/// A map of operation names to APIOperation objects with the customized documentation.
@override
Map<String, APIOperation> documentOperations(
APIDocumentContext context,
@ -193,6 +386,25 @@ class AuthController extends ResourceController {
return operations;
}
/// Defines the API responses for the token operation in OpenAPI documentation.
///
/// This method overrides the default behavior to provide custom documentation
/// for the responses of the token endpoint. It describes two possible responses:
///
/// 1. A successful response (200 OK) when credentials are successfully exchanged for a token.
/// This response includes details about the issued token such as access_token, token_type,
/// expiration time, refresh_token, and scope.
///
/// 2. An error response (400 Bad Request) for cases of invalid credentials or missing parameters.
/// This response includes an error message.
///
/// Parameters:
/// - context: The current API documentation context.
/// - operation: The operation being documented (can be null).
///
/// Returns:
/// A map of status codes to APIResponse objects describing the possible
/// responses for this operation.
@override
Map<String, APIResponse> documentOperationResponses(
APIDocumentContext context,
@ -218,6 +430,27 @@ class AuthController extends ResourceController {
};
}
/// Creates a Response object for an authentication error.
///
/// This method generates a standardized HTTP response for various authentication
/// errors that may occur during the OAuth 2.0 flow. It uses the [AuthRequestError]
/// enum to determine the specific error and creates a response with:
/// - A status code of 400 (Bad Request)
/// - A JSON body containing an "error" key with a description of the error
///
/// Parameters:
/// - error: An [AuthRequestError] enum representing the specific authentication error.
///
/// Returns:
/// A [Response] object with status code 400 and a JSON body describing the error.
///
/// Example usage:
/// ```dart
/// Response errorResponse = _responseForError(AuthRequestError.invalidRequest);
/// ```
///
/// The error string in the response body is generated using [AuthServerException.errorString],
/// ensuring consistency with OAuth 2.0 error reporting standards.
Response _responseForError(AuthRequestError error) {
return Response.badRequest(
body: {"error": AuthServerException.errorString(error)},

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:io';
@ -6,13 +15,31 @@ import 'package:protevus_auth/auth.dart';
import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart';
/// Provides [AuthRedirectController] with application-specific behavior.
/// Abstract class defining the interface for providing application-specific behavior to [AuthRedirectController].
///
/// This delegate is responsible for rendering the HTML login form when [AuthRedirectController.getAuthorizationPage]
/// is called in response to a GET request. Implementations of this class should customize the login form
/// according to the application's needs while ensuring that the form submission adheres to the required format.
///
/// The rendered form should:
/// - Be submitted as a POST request to the [requestUri].
/// - Include all provided parameters (responseType, clientID, state, scope) in the form submission.
/// - Collect and include user-entered username and password.
/// - Use 'application/x-www-form-urlencoded' as the Content-Type for form submission.
///
/// Example of expected form submission:
///
/// POST https://example.com/auth/code
/// Content-Type: application/x-www-form-urlencoded
///
/// response_type=code&client_id=com.conduit.app&state=o9u3jla&username=bob&password=password
///
/// Implementations should take care to handle all provided parameters and ensure secure transmission of credentials.
abstract class AuthRedirectControllerDelegate {
/// Returns an HTML representation of a login form.
///
/// Invoked when [AuthRedirectController.getAuthorizationPage] is called in response to a GET request.
/// Must provide HTML that will be returned to the browser for rendering. This form submission of this page
/// should be a POST to [requestUri].
/// This method is responsible for generating and returning the HTML content for a login form
/// when [AuthRedirectController.getAuthorizationPage] is called in response to a GET request.
///
/// The form submission should include the values of [responseType], [clientID], [state], [scope]
/// as well as user-entered username and password in `x-www-form-urlencoded` data, e.g.
@ -51,7 +78,23 @@ abstract class AuthRedirectControllerDelegate {
class AuthRedirectController extends ResourceController {
/// Creates a new instance of an [AuthRedirectController].
///
/// [authServer] is the required authorization server. If [delegate] is provided, this controller will return a login page for all GET requests.
/// This constructor initializes an [AuthRedirectController] with the provided [authServer].
///
/// Parameters:
/// - [authServer]: The required authorization server.
/// - [delegate]: Optional. If provided, this controller will return a login page for all GET requests.
/// - [allowsImplicit]: Optional. Defaults to true. Determines if the controller allows the Implicit Grant Flow.
///
/// The constructor also sets the [acceptedContentTypes] to ["application/x-www-form-urlencoded"].
///
/// Usage:
/// ```dart
/// final authRedirectController = AuthRedirectController(
/// myAuthServer,
/// delegate: myDelegate,
/// allowsImplicit: false,
/// );
/// ```
AuthRedirectController(
this.authServer, {
this.delegate,
@ -62,38 +105,108 @@ class AuthRedirectController extends ResourceController {
];
}
/// A pre-defined Response object for unsupported response types.
///
/// This static final variable creates a Response object with:
/// - HTTP status code 400 (Bad Request)
/// - HTML content indicating an "unsupported_response_type" error
/// - Content-Type set to text/html
///
/// This response is used when the 'response_type' parameter in the request
/// is neither 'code' nor 'token', or when 'token' is requested but implicit
/// grant flow is not allowed.
static final Response _unsupportedResponseTypeResponse = Response.badRequest(
body: "<h1>Error</h1><p>unsupported_response_type</p>",
)..contentType = ContentType.html;
/// A reference to the [AuthServer] used to grant authorization codes and access tokens.
///
/// This property holds an instance of [AuthServer] which is responsible for
/// handling the authentication and authorization processes. It is used by
/// this controller to issue authorization codes and access tokens as part of
/// the OAuth 2.0 flow.
///
/// The [AuthServer] instance should be properly configured and initialized
/// before being assigned to this property.
late final AuthServer authServer;
/// When true, the controller allows for the Implicit Grant Flow
/// Determines whether the controller allows the OAuth 2.0 Implicit Grant Flow.
///
/// When set to true, the controller will process requests for access tokens
/// directly (response_type=token). When false, such requests will be rejected.
///
/// This property is typically set in the constructor and should not be
/// modified after initialization.
final bool allowsImplicit;
/// A randomly generated value the client can use to verify the origin of the redirect.
///
/// Clients must include this query parameter and verify that any redirects from this
/// server have the same value for 'state' as passed in. This value is usually a randomly generated
/// session identifier.
/// This property is bound to the 'state' query parameter of the incoming request.
/// It serves as a security measure to prevent cross-site request forgery (CSRF) attacks.
///
/// Clients must include this query parameter when initiating an authorization request.
/// Upon receiving a redirect from this server, clients should verify that the 'state'
/// value in the redirect matches the one they initially sent. This ensures that the
/// response is for the request they initiated and not for a malicious request.
///
/// The value of 'state' is typically a randomly generated string or session identifier.
/// It should be unique for each authorization request to maintain security.
///
/// Example usage:
/// ```
/// GET /authorize?response_type=code&client_id=CLIENT_ID&state=RANDOM_STATE
/// ```
@Bind.query("state")
String? state;
/// Must be 'code' or 'token'.
/// The type of response requested from the authorization endpoint.
///
/// This property is bound to the 'response_type' query parameter of the incoming request.
/// It must be either 'code' or 'token':
/// - 'code': Indicates that the client is initiating the authorization code flow.
/// - 'token': Indicates that the client is initiating the implicit flow.
///
/// The value of this property determines the type of credential (authorization code or access token)
/// that will be issued upon successful authentication.
///
/// Note: The availability of the 'token' response type depends on the [allowsImplicit] setting
/// of the controller.
@Bind.query("response_type")
String? responseType;
/// The client ID of the authenticating client.
///
/// This must be a valid client ID according to [authServer].\
/// This property is bound to the 'client_id' query parameter of the incoming request.
/// It represents the unique identifier of the client application requesting authorization.
///
/// The client ID must be registered and valid according to the [authServer].
/// It is used to identify the client during the authorization process and to ensure
/// that only authorized clients can request access tokens or authorization codes.
///
/// This field is nullable, but typically required for most OAuth 2.0 flows.
/// If not provided in the request, it may lead to authorization failures.
///
/// Example usage in a request URL:
/// ```
/// GET /authorize?client_id=my_client_id&...
/// ```
@Bind.query("client_id")
String? clientID;
/// Renders an HTML login form.
/// Delegate responsible for rendering the HTML login form.
///
/// If provided, this delegate will be used to generate a custom login page
/// when [getAuthorizationPage] is called. The delegate's [render] method
/// is responsible for creating the HTML content of the login form.
///
/// When this property is null, the controller will not serve a login page
/// and will respond with a 405 Method Not Allowed status for GET requests.
///
/// This delegate allows for customization of the login experience while
/// maintaining the required OAuth 2.0 authorization flow.
final AuthRedirectControllerDelegate? delegate;
/// Returns an HTML login form.
/// Returns an HTML login form for OAuth 2.0 authorization.
///
/// A client that wishes to authenticate with this server should direct the user
/// to this page. The user will enter their username and password that is sent as a POST
@ -129,9 +242,10 @@ class AuthRedirectController extends ResourceController {
/// Creates a one-time use authorization code or an access token.
///
/// This method will respond with a redirect that either contains an authorization code ('code')
/// or an access token ('token') along with the passed in 'state'. If this request fails,
/// the redirect URL will contain an 'error' instead of the authorization code or access token.
/// This method handles the OAuth 2.0 authorization process, responding with a redirect
/// that contains either an authorization code ('code') or an access token ('token')
/// along with the passed in 'state'. If the request fails, the redirect URL will
/// contain an 'error' instead of the authorization code or access token.
///
/// This method is typically invoked by the login form returned from the GET to this controller.
@Operation.post()
@ -229,6 +343,19 @@ class AuthRedirectController extends ResourceController {
}
}
/// Customizes the API documentation for the request body of this controller's operations.
///
/// This method overrides the default implementation to add specific details to the
/// POST operation's request body schema:
/// - Sets the format of the 'password' field to "password".
/// - Marks 'client_id', 'state', 'response_type', 'username', and 'password' as required fields.
///
/// Parameters:
/// - context: The API documentation context.
/// - operation: The operation being documented.
///
/// Returns:
/// The modified [APIRequestBody] object, or null if no modifications were made.
@override
APIRequestBody? documentOperationRequestBody(
APIDocumentContext context,
@ -249,6 +376,20 @@ class AuthRedirectController extends ResourceController {
return body;
}
/// Customizes the API documentation for the operation parameters of this controller.
///
/// This method overrides the default implementation to mark all parameters
/// as required, except for the 'scope' parameter. It does this by:
/// 1. Calling the superclass method to get the initial list of parameters.
/// 2. Iterating through all parameters except 'scope'.
/// 3. Setting the 'isRequired' property of each parameter to true.
///
/// Parameters:
/// - context: The API documentation context.
/// - operation: The operation being documented.
///
/// Returns:
/// A list of [APIParameter] objects with updated 'isRequired' properties.
@override
List<APIParameter> documentOperationParameters(
APIDocumentContext context,
@ -261,6 +402,29 @@ class AuthRedirectController extends ResourceController {
return params;
}
/// Generates API documentation for the responses of this controller's operations.
///
/// This method overrides the default implementation to provide custom documentation
/// for the GET and POST operations of the AuthRedirectController.
///
/// For GET requests:
/// - Documents a 200 response that serves a login form in HTML format.
///
/// For POST requests:
/// - Documents a 302 (Moved Temporarily) response for successful authorizations,
/// explaining the structure of the redirect URI for both 'code' and 'token' response types.
/// - Documents a 400 (Bad Request) response for cases where the client ID is invalid
/// and the redirect URI cannot be verified.
///
/// Parameters:
/// - context: The API documentation context.
/// - operation: The operation being documented.
///
/// Returns:
/// A Map of status codes to APIResponse objects describing the possible responses.
///
/// Throws:
/// StateError if documentation fails (i.e., for unexpected HTTP methods).
@override
Map<String, APIResponse> documentOperationResponses(
APIDocumentContext context,
@ -298,6 +462,24 @@ class AuthRedirectController extends ResourceController {
throw StateError("AuthRedirectController documentation failed.");
}
/// Customizes the API documentation for the operations of this controller.
///
/// This method overrides the default implementation to update the authorization URLs
/// for both the Authorization Code Flow and Implicit Flow in the API documentation.
///
/// It performs the following steps:
/// 1. Calls the superclass method to get the initial operations documentation.
/// 2. Constructs a URI from the given route.
/// 3. Sets this URI as the authorization URL for both the Authorization Code Flow
/// and the Implicit Flow in the auth server's documentation.
///
/// Parameters:
/// - context: The API documentation context.
/// - route: The route string for this controller.
/// - path: The APIPath object representing this controller's path.
///
/// Returns:
/// A Map of operation IDs to APIOperation objects describing the operations of this controller.
@override
Map<String, APIOperation> documentOperations(
APIDocumentContext context,
@ -311,6 +493,27 @@ class AuthRedirectController extends ResourceController {
return ops;
}
/// Generates a redirect response for OAuth 2.0 authorization flow.
///
/// This method constructs a redirect URI based on the given parameters and the type of response
/// (code or token) requested. It handles both successful authorizations and error cases.
///
/// Parameters:
/// - [inputUri]: The base URI to redirect to. If null, falls back to the client's registered redirect URI.
/// - [clientStateOrNull]: The state parameter provided by the client for CSRF protection.
/// - [code]: The authorization code (for code flow).
/// - [token]: The access token (for token/implicit flow).
/// - [error]: Any error that occurred during the authorization process.
///
/// Returns:
/// - A [Response] object with a 302 status code and appropriate headers for redirection.
/// - If the redirect URI is invalid or cannot be constructed, returns a 400 Bad Request response.
///
/// The method constructs the redirect URI as follows:
/// - For 'code' response type: Adds code, state, and error (if any) as query parameters.
/// - For 'token' response type: Adds token details, state, and error (if any) as URI fragment.
///
/// The response includes headers to prevent caching of the redirect.
Response _redirectResponse(
String? inputUri,
String? clientStateOrNull, {

View file

@ -1,5 +1,23 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:convert';
/// An abstract class for parsing authorization headers.
///
/// This class defines a common interface for parsing different types of
/// authorization headers. Implementations of this class should provide
/// specific parsing logic for different authorization schemes (e.g., Bearer, Basic).
///
/// The type parameter [T] represents the return type of the [parse] method,
/// allowing for flexibility in the parsed result (e.g., String for Bearer tokens,
/// custom credential objects for other schemes).
abstract class AuthorizationParser<T> {
const AuthorizationParser();
@ -7,11 +25,24 @@ abstract class AuthorizationParser<T> {
}
/// Parses a Bearer token from an Authorization header.
///
/// This class extends [AuthorizationParser] and specializes in parsing Bearer tokens
/// from Authorization headers. It implements the [parse] method to extract the token
/// from a given header string.
///
/// Usage:
/// ```dart
/// final parser = AuthorizationBearerParser();
/// final token = parser.parse("Bearer myToken123");
/// print(token); // Outputs: myToken123
/// ```
///
/// If the header is invalid or missing, it throws an [AuthorizationParserException]
/// with an appropriate [AuthorizationParserExceptionReason].
class AuthorizationBearerParser extends AuthorizationParser<String?> {
const AuthorizationBearerParser();
/// Parses a Bearer token from [authorizationHeader]. If the header is malformed or doesn't exist,
/// throws an [AuthorizationParserException]. Otherwise, returns the [String] representation of the bearer token.
/// Parses a Bearer token from an Authorization header.
///
/// For example, if the input to this method is "Bearer token" it would return 'token'.
///
@ -37,6 +68,26 @@ class AuthorizationBearerParser extends AuthorizationParser<String?> {
/// A structure to hold Basic authorization credentials.
///
/// This class represents the credentials used in Basic HTTP Authentication.
/// It contains two properties: [username] and [password].
///
/// The [username] and [password] are marked as `late final`, indicating that
/// they must be initialized before use, but can only be set once.
///
/// This class is typically used in conjunction with [AuthorizationBasicParser]
/// to parse and store credentials from a Basic Authorization header.
///
/// The [toString] method is overridden to provide a string representation
/// of the credentials in the format "username:password".
///
/// Example usage:
/// ```dart
/// final credentials = AuthBasicCredentials()
/// ..username = 'john_doe'
/// ..password = 'secret123';
/// print(credentials); // Outputs: john_doe:secret123
/// ```
///
/// See [AuthorizationBasicParser] for getting instances of this type.
class AuthBasicCredentials {
/// The username of a Basic Authorization header.
@ -50,14 +101,38 @@ class AuthBasicCredentials {
}
/// Parses a Basic Authorization header.
///
/// This class extends [AuthorizationParser] and specializes in parsing Basic Authentication
/// credentials from Authorization headers. It implements the [parse] method to extract
/// the username and password from a given header string.
///
/// The parser expects the header to be in the format "Basic <base64-encoded-credentials>",
/// where the credentials are a string of "username:password" encoded in Base64.
///
/// Usage:
/// ```dart
/// final parser = AuthorizationBasicParser();
/// final credentials = parser.parse("Basic dXNlcm5hbWU6cGFzc3dvcmQ=");
/// print(credentials.username); // Outputs: username
/// print(credentials.password); // Outputs: password
/// ```
///
/// If the header is invalid, missing, or cannot be properly decoded, it throws an
/// [AuthorizationParserException] with an appropriate [AuthorizationParserExceptionReason].
class AuthorizationBasicParser
extends AuthorizationParser<AuthBasicCredentials> {
/// Creates a constant instance of [AuthorizationBasicParser].
///
/// This constructor allows for the creation of immutable instances of the parser,
/// which can be safely shared and reused across multiple parts of an application.
///
/// Example usage:
/// ```dart
/// final parser = const AuthorizationBasicParser();
/// ```
const AuthorizationBasicParser();
/// Returns a [AuthBasicCredentials] containing the username and password
/// base64 encoded in [authorizationHeader]. For example, if the input to this method
/// was 'Basic base64String' it would decode the base64String
/// and return the username and password by splitting that decoded string around the character ':'.
/// Parses a Basic Authorization header and returns [AuthBasicCredentials].
///
/// If [authorizationHeader] is malformed or null, throws an [AuthorizationParserException].
@override
@ -100,10 +175,49 @@ class AuthorizationBasicParser
}
}
/// The reason either [AuthorizationBearerParser] or [AuthorizationBasicParser] failed.
/// Enumerates the possible reasons for authorization parsing failures.
///
/// This enum is used in conjunction with [AuthorizationParserException] to
/// provide more specific information about why the parsing of an authorization
/// header failed.
///
/// The enum contains two values:
/// - [missing]: Indicates that the required authorization header was not present.
/// - [malformed]: Indicates that the authorization header was present but its
/// format was incorrect or could not be properly parsed.
///
/// This enum is typically used by [AuthorizationBearerParser] and
/// [AuthorizationBasicParser] to specify the nature of parsing failures.
enum AuthorizationParserExceptionReason { missing, malformed }
/// An exception indicating why Authorization parsing failed.
/// An exception class for errors encountered during authorization parsing.
///
/// This exception is thrown when there's an issue parsing an authorization header.
/// It contains a [reason] field of type [AuthorizationParserExceptionReason]
/// which provides more specific information about why the parsing failed.
///
/// The [reason] can be either [AuthorizationParserExceptionReason.missing]
/// (indicating the absence of a required authorization header) or
/// [AuthorizationParserExceptionReason.malformed] (indicating an incorrectly
/// formatted authorization header).
///
/// This exception is typically thrown by implementations of [AuthorizationParser],
/// such as [AuthorizationBearerParser] and [AuthorizationBasicParser].
///
/// Example usage:
/// ```dart
/// try {
/// parser.parse(header);
/// } catch (e) {
/// if (e is AuthorizationParserException) {
/// if (e.reason == AuthorizationParserExceptionReason.missing) {
/// print('Authorization header is missing');
/// } else if (e.reason == AuthorizationParserExceptionReason.malformed) {
/// print('Authorization header is malformed');
/// }
/// }
/// }
/// ```
class AuthorizationParserException implements Exception {
AuthorizationParserException(this.reason);

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:math';
@ -8,9 +17,10 @@ import 'package:crypto/crypto.dart';
/// A OAuth 2.0 authorization server.
///
/// An [AuthServer] is an implementation of an OAuth 2.0 authorization server. An authorization server
/// issues, refreshes and revokes access tokens. It also verifies previously issued tokens, as
/// well as client and resource owner credentials.
/// This class implements the core functionality of an OAuth 2.0 authorization server,
/// including client management, token issuance, token refresh, and token verification.
/// It supports various OAuth 2.0 flows such as password, client credentials, authorization code,
/// and refresh token.
///
/// [AuthServer]s are typically used in conjunction with [AuthController] and [AuthRedirectController].
/// These controllers provide HTTP interfaces to the [AuthServer] for issuing and refreshing tokens.
@ -57,9 +67,29 @@ import 'package:crypto/crypto.dart';
/// }
///
class AuthServer implements AuthValidator, APIComponentDocumenter {
/// Creates a new instance of an [AuthServer] with a [delegate].
/// This constructor initializes an [AuthServer] with the provided [delegate],
/// which is responsible for managing authentication-related data storage and retrieval.
///
/// [hashFunction] defaults to [sha256].
/// Parameters:
/// - [delegate]: An instance of [AuthServerDelegate] that handles data persistence.
/// - [hashRounds]: The number of iterations for password hashing. Defaults to 1000.
/// - [hashLength]: The length of the generated hash in bytes. Defaults to 32.
/// - [hashFunction]: The hash function to use. Defaults to [sha256].
///
/// The [hashRounds], [hashLength], and [hashFunction] parameters configure the
/// password hashing mechanism used by this [AuthServer] instance. These values
/// affect the security and performance of password hashing operations.
///
/// Example:
/// ```dart
/// final delegate = MyAuthServerDelegate();
/// final authServer = AuthServer(
/// delegate,
/// hashRounds: 1000,
/// hashLength: 32,
/// hashFunction: sha256,
/// );
/// ```
AuthServer(
this.delegate, {
this.hashRounds = 1000,
@ -74,32 +104,117 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
///
/// It is preferable to use the implementation of [AuthServerDelegate] from 'package:conduit_core/managed_auth.dart'. See
/// [AuthServer] for more details.
///
/// This delegate plays a crucial role in the OAuth 2.0 flow by managing the persistence
/// of authentication-related objects. It abstracts away the storage implementation,
/// allowing for flexibility in how these objects are stored (e.g., in-memory, database).
///
/// The delegate is responsible for the following main tasks:
/// 1. Storing and retrieving AuthClient information
/// 2. Managing AuthToken lifecycle (creation, retrieval, and revocation)
/// 3. Handling AuthCode operations for the authorization code flow
/// 4. Fetching ResourceOwner information for authentication purposes
///
/// Implementations of this delegate should ensure thread-safety and efficient
/// data access to maintain the performance and security of the authentication server.
final AuthServerDelegate delegate;
/// The number of hashing rounds performed by this instance when validating a password.
///
/// This value determines the number of iterations the password hashing algorithm
/// will perform. A higher number of rounds increases the computational cost and
/// time required to hash a password, making it more resistant to brute-force attacks.
/// However, it also increases the time needed for legitimate password verification.
///
/// The optimal value balances security and performance based on the specific
/// requirements of the application. Common values range from 1000 to 50000,
/// but may need adjustment based on hardware capabilities and security needs.
final int hashRounds;
/// The resulting key length of a password hash when generated by this instance.
///
/// This value determines the length (in bytes) of the generated password hash.
/// A longer hash length generally provides more security against certain types of attacks,
/// but also requires more storage space. Common values range from 16 to 64 bytes.
///
/// This parameter is used in conjunction with [hashRounds] and [hashFunction]
/// to configure the password hashing algorithm (typically PBKDF2).
final int hashLength;
/// The [Hash] function used by the PBKDF2 algorithm to generate password hashes by this instance.
///
/// This function is used in the password hashing process to create secure, one-way
/// hashes of passwords. The PBKDF2 (Password-Based Key Derivation Function 2)
/// algorithm uses this hash function repeatedly to increase the computational cost
/// of cracking the resulting hash.
///
/// By default, this is set to [sha256], but it can be customized to use other
/// cryptographic hash functions if needed. The choice of hash function affects
/// the security and performance characteristics of the password hashing process.
///
/// This property works in conjunction with [hashRounds] and [hashLength] to
/// configure the overall password hashing strategy of the AuthServer.
final Hash hashFunction;
/// Used during OpenAPI documentation.
/// Represents the OAuth 2.0 Authorization Code flow for OpenAPI documentation purposes.
///
/// This property is used to document the Authorization Code flow in the OpenAPI
/// specification generated for this AuthServer. It is initialized as an empty
/// OAuth2 flow with an empty scopes map, which can be populated later with
/// the specific scopes supported by the server.
///
/// The Authorization Code flow is a secure way of obtaining access tokens
/// that involves a client application directing the resource owner to an
/// authorization server to grant permission, then using the resulting
/// authorization code to obtain an access token.
///
/// This property is typically used in conjunction with the `documentComponents`
/// method to properly document the OAuth2 security scheme in the API specification.
final APISecuritySchemeOAuth2Flow documentedAuthorizationCodeFlow =
APISecuritySchemeOAuth2Flow.empty()..scopes = {};
/// Used during OpenAPI documentation.
/// Represents the OAuth 2.0 Password flow for OpenAPI documentation purposes.
///
/// This property is used to document the Password flow in the OpenAPI
/// specification generated for this AuthServer. It is initialized as an empty
/// OAuth2 flow with an empty scopes map, which can be populated later with
/// the specific scopes supported by the server for the Password flow.
///
/// The Password flow allows users to exchange their username and password
/// directly for an access token. This flow should only be used by trusted
/// applications due to its sensitivity in handling user credentials.
///
/// This property is typically used in conjunction with the `documentComponents`
/// method to properly document the OAuth2 security scheme in the API specification.
final APISecuritySchemeOAuth2Flow documentedPasswordFlow =
APISecuritySchemeOAuth2Flow.empty()..scopes = {};
/// Used during OpenAPI documentation.
/// Represents the OAuth 2.0 Implicit flow for OpenAPI documentation purposes.
///
/// This property is used to document the Implicit flow in the OpenAPI
/// specification generated for this AuthServer. It is initialized as an empty
/// OAuth2 flow with an empty scopes map, which can be populated later with
/// the specific scopes supported by the server for the Implicit flow.
///
/// The Implicit flow is designed for client-side applications (e.g., single-page web apps)
/// where the access token is returned immediately without an extra authorization code
/// exchange step. This flow has some security trade-offs and is generally not recommended
/// for new implementations.
///
/// This property is typically used in conjunction with the `documentComponents`
/// method to properly document the OAuth2 security scheme in the API specification.
final APISecuritySchemeOAuth2Flow documentedImplicitFlow =
APISecuritySchemeOAuth2Flow.empty()..scopes = {};
/// Constant representing the token type "bearer" for OAuth 2.0 access tokens.
///
/// This value is used to specify the type of token issued by the authorization server.
/// The "bearer" token type is defined in RFC 6750 and is the most common type used in OAuth 2.0.
/// Bearer tokens can be used by any party in possession of the token to access protected resources
/// without demonstrating possession of a cryptographic key.
static const String tokenTypeBearer = "bearer";
/// Hashes a [password] with [salt] using PBKDF2 algorithm.
/// Hashes a password using the PBKDF2 algorithm.
///
/// See [hashRounds], [hashLength] and [hashFunction] for more details. This method
/// invoke [auth.generatePasswordHash] with the above inputs.
@ -113,7 +228,7 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
);
}
/// Adds an OAuth2 client.
/// Adds a new OAuth2 client to the authentication server.
///
/// [delegate] will store this client for future use.
Future addClient(AuthClient client) async {
@ -132,14 +247,14 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
return delegate.addClient(this, client);
}
/// Returns a [AuthClient] record for its [clientID].
/// Retrieves an [AuthClient] record based on the provided [clientID].
///
/// Returns null if none exists.
Future<AuthClient?> getClient(String clientID) async {
return delegate.getClient(this, clientID);
}
/// Revokes a [AuthClient] record.
/// Revokes and removes an [AuthClient] record associated with the given [clientID].
///
/// Removes cached occurrences of [AuthClient] for [clientID].
/// Asks [delegate] to remove an [AuthClient] by its ID via [AuthServerDelegate.removeClient].
@ -151,7 +266,7 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
return delegate.removeClient(this, clientID);
}
/// Revokes access for an [ResourceOwner].
/// Revokes all access grants for a specific resource owner.
///
/// All authorization codes and tokens for the [ResourceOwner] identified by [identifier]
/// will be revoked.
@ -163,7 +278,7 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
await delegate.removeTokens(this, identifier);
}
/// Authenticates a username and password of an [ResourceOwner] and returns an [AuthToken] upon success.
/// Authenticates a username and password of a [ResourceOwner] and returns an [AuthToken] upon success.
///
/// This method works with this instance's [delegate] to generate and store a new token if all credentials are correct.
/// If credentials are not correct, it will throw the appropriate [AuthRequestError].
@ -230,7 +345,7 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
return token;
}
/// Returns a [Authorization] for [accessToken].
/// Verifies the validity of an access token and returns an [Authorization] object.
///
/// This method obtains an [AuthToken] for [accessToken] from [delegate] and then verifies that the token is valid.
/// If the token is valid, an [Authorization] object is returned. Otherwise, an [AuthServerException] is thrown.
@ -269,9 +384,34 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
/// Refreshes a valid [AuthToken] instance.
///
/// This method will refresh a [AuthToken] given the [AuthToken]'s [refreshToken] for a given client ID.
/// This method coordinates with this instance's [delegate] to update the old token with a new access token and issue/expiration dates if successful.
/// If not successful, it will throw an [AuthRequestError].
/// This method refreshes an existing [AuthToken] using its [refreshToken] for a given client ID.
/// It coordinates with the instance's [delegate] to update the old token with a new access token
/// and issue/expiration dates if successful. If unsuccessful, it throws an [AuthRequestError].
///
/// The method performs several validation steps:
/// 1. Verifies the client ID and retrieves the corresponding [AuthClient].
/// 2. Checks for the presence of a refresh token.
/// 3. Retrieves the existing token using the refresh token.
/// 4. Validates the client secret.
/// 5. Handles scope validation and updates:
/// - If new scopes are requested, it ensures they are subsets of existing scopes and allowed by the client.
/// - If no new scopes are requested, it verifies that existing scopes are still valid for the client.
///
/// Parameters:
/// - [refreshToken]: The refresh token of the [AuthToken] to be refreshed.
/// - [clientID]: The ID of the client requesting the token refresh.
/// - [clientSecret]: The secret of the client requesting the token refresh.
/// - [requestedScopes]: Optional list of scopes to be applied to the refreshed token.
///
/// Returns:
/// A [Future] that resolves to a new [AuthToken] with updated access token, issue date, and expiration date.
///
/// Throws:
/// - [AuthServerException] with [AuthRequestError.invalidClient] if the client ID is invalid or empty.
/// - [AuthServerException] with [AuthRequestError.invalidRequest] if the refresh token is missing.
/// - [AuthServerException] with [AuthRequestError.invalidGrant] if the token is not found or doesn't match the client ID.
/// - [AuthServerException] with [AuthRequestError.invalidClient] if the client secret is invalid.
/// - [AuthServerException] with [AuthRequestError.invalidScope] if the requested scopes are invalid or not allowed.
Future<AuthToken> refresh(
String? refreshToken,
String clientID,
@ -356,9 +496,34 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
/// Creates a one-time use authorization code for a given client ID and user credentials.
///
/// This methods works with this instance's [delegate] to generate and store the authorization code
/// if the credentials are correct. If they are not correct, it will throw the
/// appropriate [AuthRequestError].
/// This method is part of the OAuth 2.0 Authorization Code flow. It authenticates a user
/// with their username and password for a specific client, and if successful, generates
/// a short-lived authorization code.
///
/// The method performs several steps:
/// 1. Validates the client ID and retrieves the client information.
/// 2. Authenticates the user with the provided username and password.
/// 3. Validates the requested scopes against the client's allowed scopes and the user's permissions.
/// 4. Generates a new authorization code.
/// 5. Stores the authorization code using the delegate.
///
/// Parameters:
/// - [username]: The username of the resource owner (user).
/// - [password]: The password of the resource owner.
/// - [clientID]: The ID of the client requesting the authorization code.
/// - [expirationInSeconds]: The lifetime of the authorization code in seconds (default is 600 seconds or 10 minutes).
/// - [requestedScopes]: Optional list of scopes the client is requesting access to.
///
/// Returns:
/// A [Future] that resolves to an [AuthCode] object representing the generated authorization code.
///
/// Throws:
/// - [AuthServerException] with [AuthRequestError.invalidClient] if the client ID is invalid or empty.
/// - [AuthServerException] with [AuthRequestError.invalidRequest] if the username or password is missing.
/// - [AuthServerException] with [AuthRequestError.unauthorizedClient] if the client doesn't have a redirect URI.
/// - [AuthServerException] with [AuthRequestError.accessDenied] if the user credentials are invalid.
///
/// The generated authorization code can later be exchanged for an access token using the `exchange` method.
Future<AuthCode> authenticateForCode(
String? username,
String? password,
@ -408,9 +573,35 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
/// Exchanges a valid authorization code for an [AuthToken].
///
/// If the authorization code has not expired, has not been used, matches the client ID,
/// and the client secret is correct, it will return a valid [AuthToken]. Otherwise,
/// it will throw an appropriate [AuthRequestError].
/// This method is part of the OAuth 2.0 Authorization Code flow. It allows a client
/// to exchange a previously obtained authorization code for an access token.
///
/// The method performs several validation steps:
/// 1. Verifies the client ID and retrieves the corresponding [AuthClient].
/// 2. Checks for the presence of the authorization code.
/// 3. Validates the client secret.
/// 4. Retrieves and validates the stored authorization code.
/// 5. Checks if the authorization code is still valid and hasn't been used.
/// 6. Ensures the client ID matches the one associated with the authorization code.
///
/// If all validations pass, it generates a new access token and stores it using the delegate.
///
/// Parameters:
/// - [authCodeString]: The authorization code to be exchanged.
/// - [clientID]: The ID of the client requesting the token exchange.
/// - [clientSecret]: The secret of the client requesting the token exchange.
/// - [expirationInSeconds]: The lifetime of the generated access token in seconds (default is 3600 seconds or 1 hour).
///
/// Returns:
/// A [Future] that resolves to an [AuthToken] representing the newly created access token.
///
/// Throws:
/// - [AuthServerException] with [AuthRequestError.invalidClient] if the client ID is invalid or empty, or if the client secret is incorrect.
/// - [AuthServerException] with [AuthRequestError.invalidRequest] if the authorization code is missing.
/// - [AuthServerException] with [AuthRequestError.invalidGrant] if the authorization code is invalid, expired, or has been used before.
///
/// This method is crucial for completing the Authorization Code flow, allowing clients
/// to securely obtain access tokens after receiving user authorization.
Future<AuthToken> exchange(
String? authCodeString,
String clientID,
@ -474,6 +665,22 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
//////
// APIDocumentable overrides
//////
/// Generates and registers security schemes for API documentation.
///
/// This method is responsible for documenting the security components of the API,
/// specifically the OAuth2 client authentication and standard OAuth2 flows.
///
/// It performs the following tasks:
/// 1. Registers a basic HTTP authentication scheme for OAuth2 client authentication.
/// 2. Creates and registers an OAuth2 security scheme with authorization code and password flows.
/// 3. Defers cleanup of unused flows based on the presence of required URLs.
///
/// The method uses the [APIDocumentContext] to register these security schemes,
/// making them available for use in the API documentation.
///
/// Parameters:
/// - [context]: The [APIDocumentContext] used to register security schemes and defer cleanup operations.
@override
void documentComponents(APIDocumentContext context) {
final basic = APISecurityScheme.http("basic")
@ -508,6 +715,27 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
/////
// AuthValidator overrides
/////
/// Documents the security requirements for an [Authorizer] in the API specification.
///
/// This method generates the appropriate [APISecurityRequirement] objects
/// based on the type of authorization parser used by the [Authorizer].
///
/// For basic authentication (AuthorizationBasicParser), it specifies the
/// requirement for OAuth2 client authentication.
///
/// For bearer token authentication (AuthorizationBearerParser), it specifies
/// the requirement for OAuth2 with optional scopes.
///
/// Parameters:
/// - [context]: The API documentation context.
/// - [authorizer]: The Authorizer instance for which to generate requirements.
/// - [scopes]: Optional list of scopes to be included in the OAuth2 requirement.
///
/// Returns:
/// A list of [APISecurityRequirement] objects representing the security
/// requirements for the given authorizer. Returns an empty list if the
/// parser type is not recognized.
@override
List<APISecurityRequirement> documentRequirementsForAuthorizer(
APIDocumentContext context,
@ -529,6 +757,25 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
return [];
}
/// Validates an authorization request using the specified parser and authorization data.
///
/// This method is responsible for validating different types of authorization,
/// including client credentials (Basic) and bearer tokens.
///
/// Parameters:
/// - [parser]: An instance of [AuthorizationParser] used to parse the authorization data.
/// - [authorizationData]: The authorization data to be validated, type depends on the parser.
/// - [requiredScope]: Optional list of [AuthScope]s required for the authorization.
///
/// Returns:
/// A [FutureOr<Authorization>] representing the validated authorization.
///
/// Throws:
/// - [ArgumentError] if an invalid parser is provided.
///
/// The method behaves differently based on the type of parser:
/// - For [AuthorizationBasicParser], it validates client credentials.
/// - For [AuthorizationBearerParser], it verifies the bearer token.
@override
FutureOr<Authorization> validate<T>(
AuthorizationParser<T> parser,
@ -547,6 +794,31 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
);
}
/// Validates client credentials for OAuth 2.0 client authentication.
///
/// This method is used to authenticate a client using its client ID and secret
/// as part of the OAuth 2.0 client authentication process.
///
/// The method performs the following steps:
/// 1. Retrieves the client using the provided client ID (username).
/// 2. Validates the client's existence and secret.
/// 3. For public clients (no secret), it allows authentication with an empty password.
/// 4. For confidential clients, it verifies the provided password against the stored hashed secret.
///
/// Parameters:
/// - [credentials]: An [AuthBasicCredentials] object containing the client ID (username) and secret (password).
///
/// Returns:
/// A [Future<Authorization>] representing the authenticated client.
///
/// Throws:
/// - [AuthServerException] with [AuthRequestError.invalidClient] if:
/// - The client is not found.
/// - A public client provides a non-empty password.
/// - A confidential client provides an incorrect secret.
///
/// This method is typically used in the context of the client credentials grant type
/// or when a client needs to authenticate itself for other OAuth 2.0 flows.
Future<Authorization> _validateClientCredentials(
AuthBasicCredentials credentials,
) async {
@ -574,6 +846,29 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
return Authorization(client.id, null, this, credentials: credentials);
}
/// Validates and filters the requested scopes for a client and resource owner.
///
/// This method checks the requested scopes against the client's allowed scopes
/// and the resource owner's permitted scopes. It ensures that only valid and
/// authorized scopes are granted.
///
/// Parameters:
/// - [client]: The [AuthClient] requesting the scopes.
/// - [authenticatable]: The [ResourceOwner] being authenticated.
/// - [requestedScopes]: The list of [AuthScope]s requested by the client.
///
/// Returns:
/// A list of validated [AuthScope]s that are allowed for both the client and
/// the resource owner. Returns null if the client doesn't support scopes.
///
/// Throws:
/// - [AuthServerException] with [AuthRequestError.invalidScope] if:
/// - The client supports scopes but no scopes are requested.
/// - None of the requested scopes are allowed for the client.
/// - The filtered scopes are not allowed for the resource owner.
///
/// This method is crucial for maintaining the principle of least privilege
/// in OAuth 2.0 flows by ensuring that tokens are issued with appropriate scopes.
List<AuthScope>? _validatedScopes(
AuthClient client,
ResourceOwner authenticatable,
@ -611,6 +906,24 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
return validScopes;
}
/// Generates a new [AuthToken] with the specified parameters.
///
/// This method creates and initializes a new [AuthToken] object with the given
/// owner ID, client ID, and expiration time. It also sets other properties such
/// as the access token, issue date, token type, and optional refresh token.
///
/// Parameters:
/// - [ownerID]: The identifier of the resource owner (user).
/// - [clientID]: The identifier of the client application.
/// - [expirationInSeconds]: The number of seconds until the token expires.
/// - [allowRefresh]: Whether to generate a refresh token (default is true).
/// - [scopes]: Optional list of scopes associated with the token.
///
/// Returns:
/// A new [AuthToken] instance with all properties set according to the input parameters.
///
/// The access token and refresh token (if allowed) are generated as random strings.
/// The token type is set to "bearer" as defined by [tokenTypeBearer].
AuthToken _generateToken(
int? ownerID,
String clientID,
@ -635,6 +948,24 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
return token;
}
/// Generates a new [AuthCode] with the specified parameters.
///
/// This method creates and initializes a new [AuthCode] object with the given
/// owner ID, client, and expiration time. It also sets other properties such
/// as the authorization code, issue date, and optional scopes.
///
/// Parameters:
/// - [ownerID]: The identifier of the resource owner (user).
/// - [client]: The [AuthClient] for which the auth code is being generated.
/// - [expirationInSeconds]: The number of seconds until the auth code expires.
/// - [scopes]: Optional list of scopes associated with the auth code.
///
/// Returns:
/// A new [AuthCode] instance with all properties set according to the input parameters.
///
/// The authorization code is generated as a random string of 32 characters.
/// The issue date is set to the current UTC time, and the expiration date is
/// calculated based on the [expirationInSeconds] parameter.
AuthCode _generateAuthCode(
int? ownerID,
AuthClient client,
@ -652,6 +983,19 @@ class AuthServer implements AuthValidator, APIComponentDocumenter {
}
}
/// Generates a random string of specified length.
///
/// This function creates a random string using a combination of uppercase letters,
/// lowercase letters, and digits. It uses a cryptographically secure random number
/// generator to ensure unpredictability.
///
/// The function works by repeatedly selecting random characters from a predefined
/// set of possible characters and appending them to a string buffer. The selection
/// process uses the modulo operation to ensure an even distribution across the
/// character set.
///
/// Returns:
/// A string of the specified [length] containing random characters.
String randomStringOfLength(int length) {
const possibleCharacters =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'dart:io';
@ -8,8 +17,9 @@ import 'package:protevus_openapi/v3.dart';
/// A [Controller] that validates the Authorization header of a request.
///
/// An instance of this type will validate that the authorization information in an Authorization header is sufficient to access
/// the next controller in the channel.
/// This class, Authorizer, is responsible for authenticating and authorizing incoming HTTP requests.
/// It validates the Authorization header, processes it according to the specified parser (e.g., Bearer or Basic),
/// and then uses the provided validator to check the credentials.
///
/// For each request, this controller parses the authorization header, validates it with an [AuthValidator] and then create an [Authorization] object
/// if successful. The [Request] keeps a reference to this [Authorization] and is then sent to the next controller in the channel.
@ -28,7 +38,7 @@ import 'package:protevus_openapi/v3.dart';
class Authorizer extends Controller {
/// Creates an instance of [Authorizer].
///
/// Use this constructor to provide custom [AuthorizationParser]s.
/// This constructor allows for creating an [Authorizer] with custom configurations.
///
/// By default, this instance will parse bearer tokens from the authorization header, e.g.:
///
@ -43,7 +53,11 @@ class Authorizer extends Controller {
/// Creates an instance of [Authorizer] with Basic Authentication parsing.
///
/// Parses a username and password from the request's Basic Authentication data in the Authorization header, e.g.:
/// This constructor initializes an [Authorizer] that uses Basic Authentication.
/// It sets up the [Authorizer] to parse the Authorization header of incoming requests
/// using the [AuthorizationBasicParser].
///
/// The Authorization header for Basic Authentication should be in the format:
///
/// Authorization: Basic base64(username:password)
Authorizer.basic(AuthValidator? validator)
@ -51,7 +65,9 @@ class Authorizer extends Controller {
/// Creates an instance of [Authorizer] with Bearer token parsing.
///
/// Parses a bearer token from the request's Authorization header, e.g.
/// This constructor initializes an [Authorizer] that uses Bearer token authentication.
/// It sets up the [Authorizer] to parse the Authorization header of incoming requests
/// using the [AuthorizationBearerParser].
///
/// Authorization: Bearer ap9ijlarlkz8jIOa9laweo
///
@ -65,12 +81,19 @@ class Authorizer extends Controller {
/// The validating authorization object.
///
/// This object will check credentials parsed from the Authorization header and produce an
/// [Authorization] object representing the authorization the credentials have. It may also
/// reject a request. This is typically an instance of [AuthServer].
/// This property holds an instance of [AuthValidator] responsible for validating
/// the credentials parsed from the Authorization header. It processes these
/// credentials and produces an [Authorization] object that represents the
/// authorization level of the provided credentials.
///
/// The validator can also reject a request if the credentials are invalid or
/// insufficient. This property is typically set to an instance of [AuthServer].
///
/// The validator is crucial for determining whether a request should be allowed
/// to proceed based on the provided authorization information.
final AuthValidator? validator;
/// The list of required scopes.
/// The list of required scopes for authorization.
///
/// If [validator] grants scope-limited authorizations (e.g., OAuth2 bearer tokens), the authorization
/// provided by the request's header must have access to all [scopes] in order to move on to the next controller.
@ -79,7 +102,7 @@ class Authorizer extends Controller {
/// an [AuthScope] and added to this list.
final List<AuthScope>? scopes;
/// Parses the Authorization header.
/// Parses the Authorization header of incoming requests.
///
/// The parser determines how to interpret the data in the Authorization header. Concrete subclasses
/// are [AuthorizationBasicParser] and [AuthorizationBearerParser].
@ -87,6 +110,20 @@ class Authorizer extends Controller {
/// Once parsed, the parsed value is validated by [validator].
final AuthorizationParser parser;
/// Handles the incoming request by validating its authorization.
///
/// This method performs the following steps:
/// 1. Extracts the Authorization header from the request.
/// 2. If the header is missing, returns an unauthorized response.
/// 3. Attempts to parse the authorization data using the configured parser.
/// 4. Validates the parsed data using the configured validator.
/// 5. If validation succeeds, adds the authorization to the request and proceeds.
/// 6. If validation fails due to insufficient scope, returns a forbidden response.
/// 7. For other validation failures, returns an unauthorized response.
/// 8. Handles parsing exceptions by returning appropriate error responses.
///
/// @param request The incoming HTTP request to be authorized.
/// @return A [Future] that resolves to either the authorized [Request] or an error [Response].
@override
FutureOr<RequestOrResponse> handle(Request request) async {
final authData = request.raw.headers.value(HttpHeaders.authorizationHeader);
@ -121,6 +158,19 @@ class Authorizer extends Controller {
return request;
}
/// Generates an appropriate HTTP response based on the type of AuthorizationParserException.
///
/// This method takes an [AuthorizationParserException] as input and returns
/// a [Response] object based on the exception's reason:
///
/// - For [AuthorizationParserExceptionReason.malformed], it returns a 400 Bad Request
/// response with a body indicating an invalid authorization header.
/// - For [AuthorizationParserExceptionReason.missing], it returns a 401 Unauthorized
/// response.
/// - For any other reason, it returns a 500 Server Error response.
///
/// @param e The AuthorizationParserException that occurred during parsing.
/// @return A Response object appropriate to the exception reason.
Response _responseFromParseException(AuthorizationParserException e) {
switch (e.reason) {
case AuthorizationParserExceptionReason.malformed:
@ -134,6 +184,21 @@ class Authorizer extends Controller {
}
}
/// Adds a response modifier to the request to handle scope requirements.
///
/// This method is called after successful authorization and adds a response
/// modifier to the request. The modifier's purpose is to enhance 403 (Forbidden)
/// responses that are due to insufficient scope.
///
/// If this [Authorizer] has required scopes and the response is a 403 with a body
/// containing a "scope" key, this modifier will add any of this [Authorizer]'s
/// required scopes that aren't already present in the response body's scope list.
///
/// This ensures that if a downstream controller returns a 403 due to insufficient
/// scope, the response includes all the scopes required by both this [Authorizer]
/// and the downstream controller.
///
/// @param request The [Request] object to which the modifier will be added.
void _addScopeRequirementModifier(Request request) {
// If a controller returns a 403 because of invalid scope,
// this Authorizer adds its required scope as well.
@ -154,10 +219,49 @@ class Authorizer extends Controller {
}
}
/// Documents the components for the API documentation.
///
/// This method is responsible for registering custom API responses that are specific
/// to authorization-related errors. It adds three responses to the API documentation:
///
/// 1. "InsufficientScope": Used when the provided credentials or bearer token have
/// insufficient permissions to access a route.
///
/// 2. "InsufficientAccess": Used when the provided credentials or bearer token are
/// not authorized for a specific request.
///
/// 3. "MalformedAuthorizationHeader": Used when the provided Authorization header
/// is malformed.
///
/// Each response is registered with a description and a schema defining the
/// structure of the JSON response body.
///
/// @param context The APIDocumentContext used to register the responses.
@override
void documentComponents(APIDocumentContext context) {
/// Calls the superclass's documentComponents method.
///
/// This method invokes the documentComponents method of the superclass,
/// ensuring that any component documentation defined in the parent class
/// is properly registered in the API documentation context.
///
/// @param context The APIDocumentContext used for registering API components.
super.documentComponents(context);
/// Registers an "InsufficientScope" response in the API documentation.
///
/// This response is used when the provided credentials or bearer token
/// have insufficient permissions to access a specific route. It includes
/// details about the error and the required scope for the operation.
///
/// The response has the following structure:
/// - A description explaining the insufficient scope error.
/// - Content of type "application/json" with a schema containing:
/// - An "error" field of type string.
/// - A "scope" field of type string, describing the required scope.
///
/// This response can be referenced in API operations to standardize
/// the documentation of insufficient scope errors.
context.responses.register(
"InsufficientScope",
APIResponse(
@ -174,6 +278,19 @@ class Authorizer extends Controller {
),
);
/// Registers an "InsufficientAccess" response in the API documentation.
///
/// This response is used when the provided credentials or bearer token
/// are not authorized for a specific request. It includes details about
/// the error in a JSON format.
///
/// The response has the following structure:
/// - A description explaining the insufficient access error.
/// - Content of type "application/json" with a schema containing:
/// - An "error" field of type string.
///
/// This response can be referenced in API operations to standardize
/// the documentation of insufficient access errors.
context.responses.register(
"InsufficientAccess",
APIResponse(
@ -188,6 +305,18 @@ class Authorizer extends Controller {
),
);
/// Registers a "MalformedAuthorizationHeader" response in the API documentation.
///
/// This response is used when the provided Authorization header is malformed.
/// It includes details about the error in a JSON format.
///
/// The response has the following structure:
/// - A description explaining the malformed authorization header error.
/// - Content of type "application/json" with a schema containing:
/// - An "error" field of type string.
///
/// This response can be referenced in API operations to standardize
/// the documentation of malformed authorization header errors.
context.responses.register(
"MalformedAuthorizationHeader",
APIResponse(
@ -203,6 +332,23 @@ class Authorizer extends Controller {
);
}
/// Documents the operations for the API documentation.
///
/// This method is responsible for adding security-related responses and requirements
/// to each operation in the API documentation. It performs the following tasks:
///
/// 1. Calls the superclass's documentOperations method to get the base operations.
/// 2. For each operation:
/// - Adds a 400 response for malformed authorization headers.
/// - Adds a 401 response for insufficient access.
/// - Adds a 403 response for insufficient scope.
/// - Retrieves security requirements from the validator.
/// - Adds these security requirements to the operation.
///
/// @param context The APIDocumentContext used for documenting the API.
/// @param route The route string for which operations are being documented.
/// @param path The APIPath object representing the path of the operations.
/// @return A map of operation names to APIOperation objects with added security documentation.
@override
Map<String, APIOperation> documentOperations(
APIDocumentContext context,

View file

@ -1,11 +1,58 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_auth/auth.dart';
/// An exception thrown by [AuthServer].
/// An exception class for handling authentication server errors.
///
/// This class implements the [Exception] interface and is used to represent
/// various errors that can occur during the authentication process.
///
/// The [AuthServerException] contains:
/// - [reason]: An [AuthRequestError] enum value representing the specific error.
/// - [client]: An optional [AuthClient] associated with the error.
///
/// It also provides utility methods:
/// - [errorString]: A static method that converts [AuthRequestError] enum values to standardized error strings.
/// - [reasonString]: A getter that returns the error string for the current [reason].
///
/// The [toString] method is overridden to provide a custom string representation of the exception.
class AuthServerException implements Exception {
/// Creates an [AuthServerException] with the specified [reason] and optional [client].
///
/// The [reason] parameter is an [AuthRequestError] enum value representing the specific error.
/// The [client] parameter is an optional [AuthClient] associated with the error.
///
/// Example:
/// ```dart
/// var exception = AuthServerException(AuthRequestError.invalidRequest, null);
/// ```
AuthServerException(this.reason, this.client);
/// Returns a string suitable to be included in a query string or JSON response body
/// to indicate the error during processing an OAuth 2.0 request.
/// Converts an [AuthRequestError] enum value to its corresponding string representation.
///
/// This static method takes an [AuthRequestError] as input and returns a standardized
/// string that represents the error. These strings are suitable for inclusion in
/// query strings or JSON response bodies when indicating errors during the processing
/// of OAuth 2.0 requests.
///
/// The returned strings conform to the error codes defined in the OAuth 2.0 specification,
/// with the exception of 'invalid_token', which is a custom addition.
///
/// Example:
/// ```dart
/// var errorString = AuthServerException.errorString(AuthRequestError.invalidRequest);
/// print(errorString); // Outputs: "invalid_request"
/// ```
///
/// @param error The [AuthRequestError] enum value to convert.
/// @return A string representation of the error.
static String errorString(AuthRequestError error) {
switch (error) {
case AuthRequestError.invalidRequest:
@ -36,25 +83,54 @@ class AuthServerException implements Exception {
}
}
/// The specific reason for the authentication error.
///
/// This property holds an [AuthRequestError] enum value that represents
/// the specific error that occurred during the authentication process.
/// It provides detailed information about why the authentication request failed.
AuthRequestError reason;
/// The optional [AuthClient] associated with this exception.
///
/// This property may contain an [AuthClient] instance that is related to the
/// authentication error. It can be null if no specific client is associated
/// with the error or if the error occurred before client authentication.
///
/// This information can be useful for debugging or logging purposes, providing
/// context about which client encountered the authentication error.
AuthClient? client;
/// Returns a string representation of the [reason] for this exception.
///
/// This getter utilizes the static [errorString] method to convert the
/// [AuthRequestError] enum value stored in [reason] to its corresponding
/// string representation.
///
/// @return A standardized string representation of the error reason.
String get reasonString {
return errorString(reason);
}
/// Returns a string representation of the [AuthServerException].
///
/// This method overrides the default [Object.toString] method to provide
/// a custom string representation of the exception. The returned string
/// includes the exception class name, the [reason] for the exception,
/// and the associated [client] (if any).
///
/// @return A string in the format "AuthServerException: [reason] [client]".
@override
String toString() {
return "AuthServerException: $reason $client";
}
}
/// The possible errors as defined by the OAuth 2.0 specification.
/// Enum representing possible errors as defined by the OAuth 2.0 specification.
///
/// Auth endpoints will use this list of values to determine the response sent back
/// to a client upon a failed request.
enum AuthRequestError {
/// The request was invalid...
/// Represents an invalid request error.
///
/// The request is missing a required parameter, includes an
/// unsupported parameter value (other than grant type),
@ -63,7 +139,7 @@ enum AuthRequestError {
/// client, or is otherwise malformed.
invalidRequest,
/// The client was invalid...
/// Represents an invalid client error.
///
/// Client authentication failed (e.g., unknown client, no
/// client authentication included, or unsupported
@ -77,7 +153,7 @@ enum AuthRequestError {
/// matching the authentication scheme used by the client.
invalidClient,
/// The grant was invalid...
/// Represents an invalid grant error.
///
/// The provided authorization grant (e.g., authorization
/// code, resource owner credentials) or refresh token is
@ -86,36 +162,81 @@ enum AuthRequestError {
/// another client.
invalidGrant,
/// The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner.
/// Represents an invalid scope error.
///
/// This error occurs when the requested scope is invalid, unknown, malformed,
/// or exceeds the scope granted by the resource owner. It typically indicates
/// that the client has requested access to resources or permissions that are
/// either not recognized by the authorization server or not authorized for
/// the particular client or user.
///
/// In the OAuth 2.0 flow, this error might be returned if a client requests
/// access to a scope that doesn't exist or that the user hasn't granted
/// permission for.
invalidScope,
/// The authorization grant type is not supported by the authorization server.
/// Represents an unsupported grant type error.
///
/// This error occurs when the authorization server does not support the
/// grant type requested by the client. It typically indicates that the
/// client has specified a grant type that is either not recognized or
/// not implemented by the authorization server.
///
/// In the OAuth 2.0 flow, this error might be returned if, for example,
/// a client requests a grant type like "password" when the server only
/// supports "authorization_code" and "refresh_token" grant types.
unsupportedGrantType,
/// The authorization server does not support obtaining an authorization code using this method.
/// Represents an unsupported response type error.
///
/// This error occurs when the authorization server does not support obtaining
/// an authorization code using the specified response type. It typically
/// indicates that the client has requested a response type that is not
/// recognized or not implemented by the authorization server.
unsupportedResponseType,
/// The authenticated client is not authorized to use this authorization grant type.
/// Represents an unauthorized client error.
///
/// This error occurs when the client is not authorized to request an
/// authorization code using this method. It typically indicates that
/// the client does not have the necessary permissions or credentials
/// to perform the requested action, even though it may be properly
/// authenticated.
unauthorizedClient,
/// The resource owner or authorization server denied the request.
/// Represents an access denied error.
///
/// This error occurs when the resource owner or authorization server denies the request.
/// It is typically used when the authenticated user does not have sufficient permissions
/// to perform the requested action, or when the user explicitly denies authorization
/// during the OAuth flow.
accessDenied,
/// The server encountered an error during processing the request.
/// Represents a server error.
///
/// This error occurs when the authorization server encounters an unexpected
/// condition that prevented it from fulfilling the request. This is typically
/// used for internal server errors or other unexpected issues that prevent
/// the server from properly processing the authentication request.
serverError,
/// The server is temporarily unable to fulfill the request.
/// Represents a temporarily unavailable error.
///
/// This error occurs when the authorization server is temporarily unable to handle
/// the request due to a temporary overloading or maintenance of the server.
/// The client may repeat the request at a later time. The server SHOULD include
/// a Retry-After HTTP header field in the response indicating how long the client
/// should wait before retrying the request.
temporarilyUnavailable,
/// Indicates that the token is invalid.
/// Represents an invalid token error.
///
/// This particular error reason is not part of the OAuth 2.0 spec.
/// This error occurs when the provided token is invalid, expired, or otherwise
/// not acceptable for the requested operation. It is typically used when a client
/// presents an access token that cannot be validated or is no longer valid.
///
/// Note: This particular error reason is not part of the standard OAuth 2.0
/// specification. It is a custom addition to handle scenarios specific to
/// token validation that are not covered by other standard error types.
invalidToken
}

View file

@ -1,20 +1,32 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'package:protevus_auth/auth.dart';
import 'package:protevus_http/http.dart';
/// Represents an OAuth 2.0 client ID and secret pair.
///
/// See the conduit/managed_auth library for a concrete implementation of this type.
/// This class encapsulates the information necessary for OAuth 2.0 client authentication.
/// It can represent both public and confidential clients, with support for the authorization code grant flow.
///
/// Use the command line tool `conduit auth` to create instances of this type and store them to a database.
class AuthClient {
/// Creates an instance of [AuthClient].
///
/// [id] must not be null. [hashedSecret] and [salt] must either both be null or both be valid values. If [hashedSecret] and [salt]
/// are valid values, this client is a confidential client. Otherwise, the client is public. The terms 'confidential' and 'public'
/// are described by the OAuth 2.0 specification.
/// This constructor creates an [AuthClient] with the given parameters.
///
/// If this client supports scopes, [allowedScopes] must contain a list of scopes that tokens may request when authorized
/// by this client.
///
/// NOTE: [id] must not be null. [hashedSecret] and [salt] must either both be null or both be valid values. If [hashedSecret] and [salt]
/// are valid values, this client is a confidential client. Otherwise, the client is public. The terms 'confidential' and 'public'
/// are described by the OAuth 2.0 specification.
AuthClient(
String id,
String? hashedSecret,
@ -29,6 +41,16 @@ class AuthClient {
);
/// Creates an instance of a public [AuthClient].
///
/// This constructor creates a public [AuthClient] with the given [id].
/// Public clients do not have a client secret.
///
/// - [id]: The unique identifier for the client.
/// - [allowedScopes]: Optional list of scopes that this client is allowed to request.
/// - [redirectURI]: Optional URI to redirect to after authorization.
///
/// This is equivalent to calling [AuthClient.withRedirectURI] with null values for
/// hashedSecret and salt.
AuthClient.public(String id,
{List<AuthScope>? allowedScopes, String? redirectURI})
: this.withRedirectURI(
@ -41,7 +63,17 @@ class AuthClient {
/// Creates an instance of [AuthClient] that uses the authorization code grant flow.
///
/// All values must be non-null. This is confidential client.
/// This constructor creates a confidential [AuthClient] with the given parameters.
///
/// - [id]: The unique identifier for the client.
/// - [hashedSecret]: The hashed secret of the client.
/// - [salt]: The salt used to hash the client secret.
/// - [redirectURI]: The URI to redirect to after authorization.
/// - [allowedScopes]: Optional list of scopes that this client is allowed to request.
///
/// This constructor is specifically for clients that use the authorization code grant flow,
/// which requires a redirect URI. All parameters except [allowedScopes] must be non-null.
/// The presence of [hashedSecret] and [salt] indicates that this is a confidential client.
AuthClient.withRedirectURI(
this.id,
this.hashedSecret,
@ -52,28 +84,57 @@ class AuthClient {
this.allowedScopes = allowedScopes;
}
/// The list of allowed scopes for this client.
///
/// This private variable stores the allowed scopes for the AuthClient.
/// It is used internally to manage and validate the scopes that this client
/// is authorized to request during the authentication process.
List<AuthScope>? _allowedScopes;
/// The ID of the client.
/// The unique identifier for this OAuth 2.0 client.
///
/// This is a required field for all OAuth 2.0 clients and is used to identify
/// the client during the authentication and authorization process. It should
/// be a string value that is unique among all clients registered with the
/// authorization server.
final String id;
/// The hashed secret of the client.
///
/// This value may be null if the client is public. See [isPublic].
/// This property stores the hashed version of the client's secret, which is used for authentication
/// in confidential clients. The secret is hashed for security reasons, to avoid storing the raw secret.
///
/// This value may be null if the client is public. A null value indicates that this is a public client,
/// which doesn't use a client secret for authentication. See [isPublic] for more information on
/// determining if a client is public or confidential.
///
/// The hashed secret is typically used in conjunction with the [salt] property to verify
/// the client's credentials during the authentication process.
String? hashedSecret;
/// The salt [hashedSecret] was hashed with.
/// The salt used to hash the client secret.
///
/// This value may be null if the client is public. See [isPublic].
String? salt;
/// The redirection URI for authorization codes and/or tokens.
///
/// This property stores the URI where the authorization server should redirect
/// the user after they grant or deny permission to the client. It is used in
/// the authorization code grant flow of OAuth 2.0.
///
/// In the context of OAuth 2.0:
/// - For authorization code grant, this URI is where the authorization code is sent.
/// - For implicit grant, this URI is where the access token is sent.
///
/// This value may be null if the client doesn't support the authorization code flow.
String? redirectURI;
/// The list of scopes available when authorizing with this client.
///
/// This getter returns the list of allowed scopes for the client. The setter
/// filters the provided list to remove any redundant scopes.
///
/// Scoping is determined by this instance; i.e. the authorizing client determines which scopes a token
/// has. This list contains all valid scopes for this client. If null, client does not support scopes
/// and all access tokens have same authorization.
@ -87,32 +148,54 @@ class AuthClient {
}).toList();
}
/// Whether or not this instance allows scoping or not.
/// Determines if this instance supports authorization scopes.
///
/// In application's that do not use authorization scopes, this will return false.
/// Otherwise, will return true.
bool get supportsScopes => allowedScopes != null;
/// Whether or not this client can issue tokens for the provided [scope].
/// Determines if this client can issue tokens for the provided [scope].
///
/// This method checks if the given [scope] is allowed for this client by comparing it
/// against the client's [allowedScopes]. It returns true if the provided [scope] is
/// a subset of or equal to any of the scopes in [allowedScopes].
///
/// If [allowedScopes] is null or empty, this method returns false, indicating that
/// no scopes are allowed for this client.
///
/// [scope]: The AuthScope to check against this client's allowed scopes.
///
/// Returns true if the scope is allowed, false otherwise.
bool allowsScope(AuthScope scope) {
return allowedScopes
?.any((clientScope) => scope.isSubsetOrEqualTo(clientScope)) ??
false;
}
/// Whether or not this is a public or confidential client.
/// Whether or not this is a public client.
///
/// Public clients do not have a client secret and are used for clients that can't store
/// their secret confidentially, i.e. JavaScript browser applications.
bool get isPublic => hashedSecret == null;
/// Whether or not this is a public or confidential client.
/// Determines whether this client is confidential or public.
///
/// Confidential clients have a client secret that must be used when authenticating with
/// a client-authenticated request. Confidential clients are used when you can
/// be sure that the client secret cannot be viewed by anyone outside of the developer.
bool get isConfidential => hashedSecret != null;
/// Returns a string representation of the AuthClient instance.
///
/// This method provides a human-readable description of the AuthClient, including:
/// - Whether the client is public or confidential
/// - The client's ID
/// - The client's redirect URI (if set)
///
/// The format of the returned string is:
/// "AuthClient (public/confidential): [client_id] [redirect_uri]"
///
/// @return A string representation of the AuthClient.
@override
String toString() {
return "AuthClient (${isPublic ? "public" : "confidental"}): $id $redirectURI";
@ -121,24 +204,67 @@ class AuthClient {
/// Represents an OAuth 2.0 token.
///
/// [AuthServerDelegate] and [AuthServer] will exchange OAuth 2.0
/// tokens through instances of this type.
/// This class encapsulates the properties and functionality of an OAuth 2.0 token,
/// including access token, refresh token, expiration details, and associated scopes.
/// It is used by [AuthServerDelegate] and [AuthServer] to exchange OAuth 2.0 tokens.
///
/// See the `package:conduit_core/managed_auth` library for a concrete implementation of this type.
class AuthToken {
/// The value to be passed as a Bearer Authorization header.
/// The access token string for OAuth 2.0 authentication.
///
/// This token is used in the Authorization header of HTTP requests to authenticate
/// the client. It should be included in the header as "Bearer <accessToken>".
///
/// The access token is typically a short-lived credential that grants access to
/// protected resources on behalf of the resource owner (user).
///
/// This value may be null if the token has not been issued or has been invalidated.
String? accessToken;
/// The value to be passed for refreshing a token.
/// The refresh token associated with this OAuth 2.0 token.
///
/// A refresh token is a credential that can be used to obtain a new access token
/// when the current access token becomes invalid or expires. This allows the client
/// to obtain continued access to protected resources without requiring the resource
/// owner to re-authorize the application.
///
/// This value may be null if the authorization server does not issue refresh tokens
/// or if the token has not been issued with a refresh token.
String? refreshToken;
/// The time this token was issued on.
///
/// This property represents the date and time when the OAuth 2.0 token was originally issued.
/// It can be used to calculate the age of the token or to implement token refresh policies.
/// The value is stored as a [DateTime] object, which allows for easy manipulation and comparison.
///
/// This value may be null if the issue date is not tracked or has not been set.
DateTime? issueDate;
/// The time when this token expires.
/// The expiration date and time of this token.
///
/// This property represents the point in time when the OAuth 2.0 token will become invalid.
/// After this date and time, the token should no longer be accepted for authentication.
///
/// The value is stored as a [DateTime] object, which allows for easy comparison with the current time
/// to determine if the token has expired. This is typically used in conjunction with [issueDate]
/// to calculate the token's lifespan and manage token refresh cycles.
///
/// This value may be null if the token does not have an expiration date or if it has not been set.
DateTime? expirationDate;
/// The type of token, currently only 'bearer' is valid.
/// The type of token used for authentication.
///
/// This property specifies the type of token being used. In the OAuth 2.0 framework,
/// the most common token type is 'bearer'. The token type is typically used in the
/// HTTP Authorization header to indicate how the access token should be used.
///
/// Currently, only 'bearer' is considered valid for this implementation.
///
/// Example usage in an HTTP header:
/// Authorization: Bearer <access_token>
///
/// This value may be null if the token type has not been set or is unknown.
String? type;
/// The identifier of the resource owner.
@ -146,15 +272,60 @@ class AuthToken {
/// Tokens are owned by a resource owner, typically a User, Profile or Account
/// in an application. This value is the primary key or identifying value of those
/// instances.
///
/// This property represents the unique identifier of the resource owner associated
/// with the OAuth 2.0 token. It is typically used to link the token to a specific
/// user or account in the system.
///
/// The value is stored as an integer, which could be:
/// - A database primary key
/// - A unique user ID
/// - Any other numeric identifier that uniquely identifies the resource owner
///
/// This property may be null if the token is not associated with a specific
/// resource owner or if the association has not been established.
int? resourceOwnerIdentifier;
/// The client ID this token was issued from.
/// The client ID associated with this token.
///
/// This property represents the unique identifier of the OAuth 2.0 client
/// that was used to obtain this token. It is used to link the token back
/// to the client application that requested it.
///
/// The client ID is typically assigned by the authorization server when
/// the client application is registered, and it's used to identify the
/// client during the authentication and token issuance process.
late String clientID;
/// Scopes this token has access to.
/// The list of authorization scopes associated with this token.
///
/// This property represents the set of permissions or access rights granted to this token.
/// Each [AuthScope] in the list defines a specific area of access or functionality
/// that the token holder is allowed to use.
///
/// The scopes determine what actions or resources the token can access within the system.
/// If this list is null, it typically means the token has no specific scope restrictions
/// and may have full access (depending on the system's implementation).
///
/// This property is crucial for implementing fine-grained access control in OAuth 2.0
/// systems, allowing for precise definition of what each token is allowed to do.
List<AuthScope>? scopes;
/// Whether or not this token is expired by evaluated [expirationDate].
/// Determines whether this token has expired.
///
/// This getter compares the token's [expirationDate] with the current UTC time
/// to determine if the token has expired. It returns true if the token has
/// expired, and false if it is still valid.
///
/// The comparison is done by calculating the difference in seconds between
/// the expiration date and the current time. If this difference is less than
/// or equal to zero, the token is considered expired.
///
/// Returns:
/// [bool]: true if the token has expired, false otherwise.
///
/// Note: This getter assumes that [expirationDate] is not null. If it is null,
/// this will result in a null pointer exception.
bool get isExpired {
return expirationDate!.difference(DateTime.now().toUtc()).inSeconds <= 0;
}
@ -182,37 +353,118 @@ class AuthToken {
/// Represents an OAuth 2.0 authorization code.
///
/// [AuthServerDelegate] and [AuthServer] will exchange OAuth 2.0
/// authorization codes through instances of this type.
/// This class encapsulates the properties and functionality of an OAuth 2.0 authorization code,
/// which is used in the authorization code grant flow. It contains information such as the code itself,
/// associated client and resource owner details, issue and expiration dates, and requested scopes.
///
/// See the conduit/managed_auth library for a concrete implementation of this type.
class AuthCode {
/// The actual one-time code used to exchange for tokens.
///
/// This property represents the authorization code in the OAuth 2.0 authorization code flow.
/// It is a short-lived, single-use code that is issued by the authorization server and can be
/// exchanged for an access token and, optionally, a refresh token.
///
/// The code is typically valid for a short period (usually a few minutes) and can only be
/// used once. After it has been exchanged for tokens, it becomes invalid.
///
/// This value may be null if the code has not been generated yet or has been invalidated.
String? code;
/// The client ID the authorization code was issued under.
/// The client ID associated with this authorization code.
///
/// This property represents the unique identifier of the OAuth 2.0 client
/// that requested the authorization code. It is used to link the authorization
/// code back to the client application that initiated the OAuth flow.
///
/// The client ID is typically assigned by the authorization server when
/// the client application is registered, and it's used to identify the
/// client during the authorization code exchange process.
///
/// This property is marked as 'late', indicating that it must be initialized
/// before it's accessed, but not necessarily in the constructor.
late String clientID;
/// The identifier of the resource owner.
/// The identifier of the resource owner associated with this authorization code.
///
/// Authorization codes are owned by a resource owner, typically a User, Profile or Account
/// in an application. This value is the primary key or identifying value of those
/// instances.
int? resourceOwnerIdentifier;
/// The timestamp this authorization code was issued on.
/// The timestamp when this authorization code was issued.
///
/// This property represents the date and time when the OAuth 2.0 authorization code
/// was originally created and issued by the authorization server. It can be used to:
/// - Calculate the age of the authorization code
/// - Implement expiration policies
/// - Audit the authorization process
///
/// The value is stored as a [DateTime] object, which allows for easy manipulation
/// and comparison with other dates and times.
///
/// This value may be null if the issue date is not tracked or has not been set.
DateTime? issueDate;
/// When this authorization code expires, recommended for 10 minutes after issue date.
/// The expiration date and time of this authorization code.
///
/// This property represents the point in time when the OAuth 2.0 authorization code
/// will become invalid. After this date and time, the code should no longer be
/// accepted for token exchange.
///
/// It is recommended to set this value to 10 minutes after the [issueDate] to
/// limit the window of opportunity for potential attacks using intercepted
/// authorization codes.
///
/// The value is stored as a [DateTime] object, which allows for easy comparison
/// with the current time to determine if the code has expired. This is typically
/// used in conjunction with [issueDate] to enforce the short-lived nature of
/// authorization codes.
///
/// This value may be null if the authorization code does not have an expiration
/// date or if it has not been set.
DateTime? expirationDate;
/// Whether or not this authorization code has already been exchanged for a token.
/// Indicates whether this authorization code has already been exchanged for a token.
///
/// In the OAuth 2.0 authorization code flow, an authorization code should only be used once
/// to obtain an access token. This property helps track whether the code has been exchanged.
///
/// - If `true`, the code has already been used to obtain a token and should not be accepted again.
/// - If `false` or `null`, the code has not yet been exchanged and may still be valid for token issuance.
///
/// This property is crucial for preventing authorization code replay attacks, where an attacker
/// might attempt to use a single authorization code multiple times.
bool? hasBeenExchanged;
/// Scopes the exchanged token will have.
/// The list of scopes requested for the token to be exchanged.
///
/// This property represents the set of permissions or access rights that are being
/// requested for the OAuth 2.0 token during the authorization code exchange process.
/// Each [AuthScope] in the list defines a specific area of access or functionality
/// that the token is requesting to use.
///
/// If this list is null, it typically means no specific scopes are being requested,
/// and the token may receive default scopes or full access (depending on the system's
/// implementation and configuration).
///
/// The actual scopes granted to the token may be a subset of these requested scopes,
/// based on the authorization server's policies and the resource owner's consent.
List<AuthScope>? requestedScopes;
/// Whether or not this code has expired yet, according to its [expirationDate].
/// Determines whether this authorization code has expired.
///
/// This getter compares the [expirationDate] of the authorization code with the current UTC time
/// to determine if the code has expired. It returns true if the code has expired, and false if it is still valid.
///
/// The comparison is done by calculating the difference in seconds between the expiration date and the current time.
/// If this difference is less than or equal to zero, the code is considered expired.
///
/// Returns:
/// [bool]: true if the authorization code has expired, false otherwise.
///
/// Note: This getter assumes that [expirationDate] is not null. If it is null,
/// this will result in a null pointer exception.
bool get isExpired {
return expirationDate!.difference(DateTime.now().toUtc()).inSeconds <= 0;
}
@ -220,12 +472,34 @@ class AuthCode {
/// Authorization information for a [Request] after it has passed through an [Authorizer].
///
/// This class encapsulates various pieces of authorization information, including:
/// - The client ID under which the permission was granted
/// - The identifier for the resource owner (if applicable)
/// - The [AuthValidator] that granted the permission
/// - Basic authorization credentials (if provided)
/// - A list of scopes that this authorization has access to
///
/// It also provides a method to check if the authorization has access to a specific scope.
///
/// This class is typically used in conjunction with [Authorizer] and [AuthValidator]
/// to manage and verify authorization in a request-response cycle.
/// After a request has passed through an [Authorizer], an instance of this type
/// is created and attached to the request (see [Request.authorization]). Instances of this type contain the information
/// that the [Authorizer] obtained from an [AuthValidator] (typically an [AuthServer])
/// about the validity of the credentials in a request.
class Authorization {
/// Creates an instance of a [Authorization].
/// Creates an instance of [Authorization].
///
/// This constructor initializes an [Authorization] object with the provided parameters:
///
/// - [clientID]: The client ID under which the permission was granted.
/// - [ownerID]: The identifier for the owner of the resource, if provided. Can be null.
/// - [validator]: The [AuthValidator] that granted this permission.
/// - [credentials]: Optional. Basic authorization credentials, if provided.
/// - [scopes]: Optional. The list of scopes this authorization has access to.
///
/// This class is typically used to represent the authorization information for a [Request]
/// after it has passed through an [Authorizer].
Authorization(
this.clientID,
this.ownerID,
@ -234,34 +508,71 @@ class Authorization {
this.scopes,
});
/// The client ID the permission was granted under.
/// The client ID associated with this authorization.
///
/// This property represents the unique identifier of the OAuth 2.0 client
/// that was granted permission. It is used to link the authorization
/// back to the specific client application that requested it.
///
/// The client ID is typically assigned by the authorization server when
/// the client application is registered, and it's used to identify the
/// client throughout the OAuth 2.0 flow.
final String clientID;
/// The identifier for the owner of the resource, if provided.
///
/// If this instance refers to the authorization of a resource owner, this value will
/// be its identifying value. For example, in an application where a 'User' is stored in a database,
/// this value would be the primary key of that user.
/// This property represents the unique identifier of the resource owner associated
/// with this authorization. In OAuth 2.0 terminology, the resource owner is typically
/// the end-user who grants permission to an application to access their data.
///
/// If this authorization does not refer to a specific resource owner, this value will be null.
final int? ownerID;
/// The [AuthValidator] that granted this permission.
///
/// This property represents the [AuthValidator] instance that was responsible
/// for validating and granting the authorization. It can be used to trace
/// the origin of the authorization or to perform additional validation
/// if needed.
///
/// The validator might be null in cases where the authorization was not
/// granted through a standard validation process or if the information
/// about the validator is not relevant or available.
final AuthValidator? validator;
/// Basic authorization credentials, if provided.
///
/// If this instance represents the authorization header of a request with basic authorization credentials,
/// the parsed credentials will be available in this property. Otherwise, this value is null.
/// This property holds the parsed basic authorization credentials if they were
/// present in the authorization header of the request. If the request did not
/// use basic authorization, or if the credentials were not successfully parsed,
/// this property will be null.
///
/// The [AuthBasicCredentials] object typically contains a username and password
/// pair extracted from the 'Authorization' header of an HTTP request using the
/// Basic authentication scheme.
///
/// This can be useful for endpoints that support both OAuth 2.0 token-based
/// authentication and traditional username/password authentication via Basic Auth.
final AuthBasicCredentials? credentials;
/// The list of scopes this authorization has access to.
///
/// If the access token used to create this instance has scope,
/// those scopes will be available here. Otherwise, null.
/// This property represents the set of permissions or access rights granted to this authorization.
/// Each [AuthScope] in the list defines a specific area of access or functionality
/// that the authorization is allowed to use.
///
/// If the access token used to create this instance has scopes associated with it,
/// those scopes will be available in this list. If no scopes were associated with
/// the access token, or if scopes are not being used in the system, this property will be null.
///
/// Scopes are crucial for implementing fine-grained access control in OAuth 2.0 systems,
/// allowing for precise definition of what each authorization is allowed to do.
///
/// This list can be used in conjunction with the [isAuthorizedForScope] method to check
/// if the authorization has access to a specific scope.
List<AuthScope>? scopes;
/// Whether or not this instance has access to a specific scope.
/// Determines if this authorization has access to a specific scope.
///
/// This method checks each element in [scopes] for any that gives privileges
/// to access [scope].
@ -271,7 +582,7 @@ class Authorization {
}
}
/// Instances represent OAuth 2.0 scope.
/// Represents and manages OAuth 2.0 scopes.
///
/// An OAuth 2.0 token may optionally have authorization scopes. An authorization scope provides more granular
/// authorization to protected resources. Without authorization scopes, any valid token can pass through an
@ -282,7 +593,7 @@ class Authorization {
/// any of the scopes the client provides. Scopes are then granted to the access token. An [Authorizer] may specify
/// a one or more required scopes that a token must have to pass to the next controller.
class AuthScope {
/// Creates an instance of this type from [scopeString].
/// Creates an instance of [AuthScope] from a [scopeString].
///
/// A simple authorization scope string is a single keyword. Valid characters are
///
@ -320,6 +631,27 @@ class AuthScope {
return scope;
}
/// Parses and creates an [AuthScope] instance from a given scope string.
///
/// This factory method performs several validation checks on the input [scopeString]:
/// 1. Ensures the string is not empty.
/// 2. Validates that each character in the string is within the allowed set of characters.
/// 3. Parses the string into segments and extracts the modifier (if any).
///
/// The allowed characters are: A-Za-z0-9!#$%&'`()*+,./:;<=>?@[]^_{|}-
///
/// If any validation fails, a [FormatException] is thrown with a descriptive error message.
///
/// After successful validation and parsing, it creates and returns a new [AuthScope] instance.
///
/// Parameters:
/// [scopeString]: The string representation of the scope to parse.
///
/// Returns:
/// A new [AuthScope] instance representing the parsed scope.
///
/// Throws:
/// [FormatException] if the [scopeString] is empty or contains invalid characters.
factory AuthScope._parse(String scopeString) {
if (scopeString.isEmpty) {
throw FormatException(
@ -345,16 +677,28 @@ class AuthScope {
return AuthScope._(scopeString, segments, lastModifier);
}
/// Private constructor for creating an [AuthScope] instance.
///
/// This constructor is used internally by the class to create instances
/// after parsing and validating the scope string.
///
/// Parameters:
/// [_scopeString]: The original, unparsed scope string.
/// [_segments]: A list of parsed [_AuthScopeSegment] objects representing the scope's segments.
/// [_lastModifier]: The modifier of the last segment, if any.
///
/// This constructor is marked as `const` to allow for compile-time constant instances,
/// which can improve performance and memory usage in certain scenarios.
const AuthScope._(this._scopeString, this._segments, this._lastModifier);
/// Signifies 'any' scope in [AuthServerDelegate.getAllowedScopes].
/// Represents a special constant for indicating 'any' scope in [AuthServerDelegate.getAllowedScopes].
///
/// See [AuthServerDelegate.getAllowedScopes] for more details.
static const List<AuthScope> any = [
AuthScope._("_scope:_constant:_marker", [], null)
];
/// Returns true if that [providedScopes] fulfills [requiredScopes].
/// Verifies if the provided scopes fulfill the required scopes.
///
/// For all [requiredScopes], there must be a scope in [requiredScopes] that meets or exceeds
/// that scope for this method to return true. If [requiredScopes] is null, this method
@ -375,23 +719,97 @@ class AuthScope {
});
}
/// A cache to store previously created AuthScope instances.
///
/// This static map serves as a cache to store AuthScope instances that have been
/// previously created. The key is the string representation of the scope, and
/// the value is the corresponding AuthScope instance.
///
/// Caching AuthScope instances can improve performance by avoiding repeated
/// parsing and object creation for frequently used scopes. When an AuthScope
/// is requested with a scope string that already exists in this cache, the
/// cached instance is returned instead of creating a new one.
static final Map<String, AuthScope> _cache = {};
/// The original, unparsed scope string.
///
/// This private field stores the complete scope string as it was originally provided
/// when creating the AuthScope instance. It represents the full, unmodified scope
/// including all segments and modifiers.
///
/// This string is used for caching purposes and when converting the AuthScope
/// back to its string representation (e.g., in the toString() method).
final String _scopeString;
/// Individual segments, separated by `:` character, of this instance.
/// Returns an iterable of individual segments of this AuthScope instance.
///
/// Will always have a length of at least 1.
Iterable<String?> get segments => _segments.map((s) => s.name);
/// The modifier of this scope, if it exists.
/// Returns the modifier of this scope, if it exists.
///
/// If this instance does not have a modifier, returns null.
/// The modifier is an optional component of an AuthScope that provides additional
/// specification or restriction to the scope. It is typically the last part of a
/// scope string, following a dot (.) after the last segment.
///
/// For example, in the scope "user:profile.readonly", "readonly" is the modifier.
///
/// Returns:
/// A [String] representing the modifier if one exists, or null if this AuthScope
/// does not have a modifier.
///
/// This getter provides access to the private [_lastModifier] field, allowing
/// external code to check for the presence and value of a modifier without
/// directly accessing the internal state of the AuthScope.
String? get modifier => _lastModifier;
/// List of segments that make up this AuthScope.
///
/// This private field stores the parsed segments of the scope string as a list of
/// [_AuthScopeSegment] objects. Each segment represents a part of the scope,
/// separated by colons in the original scope string.
///
/// For example, for a scope string "user:profile:read", this list would contain
/// three _AuthScopeSegment objects representing "user", "profile", and "read"
/// respectively.
///
/// This list is used internally for scope comparisons and validations.
final List<_AuthScopeSegment> _segments;
/// The modifier of the last segment in this AuthScope.
///
/// This private field stores the modifier of the last segment in the AuthScope,
/// if one exists. A modifier provides additional specification or restriction
/// to a scope and is typically the part following a dot (.) in the last segment
/// of a scope string.
///
/// For example, in the scope "user:profile.readonly", "readonly" would be stored
/// in this field.
///
/// The value is null if the AuthScope does not have a modifier in its last segment.
final String? _lastModifier;
/// Parses the given scope string into a list of [_AuthScopeSegment] objects.
///
/// This method performs the following steps:
/// 1. Checks if the input string is empty and throws a [FormatException] if it is.
/// 2. Splits the string by ':' and creates [_AuthScopeSegment] objects for each segment.
/// 3. Validates each segment, ensuring:
/// - Only the last segment can have a modifier.
/// - There are no empty segments.
/// - There are no leading or trailing colons.
///
/// If any validation fails, a [FormatException] is thrown with a descriptive error message
/// and the position in the string where the error occurred.
///
/// Parameters:
/// [scopeString]: The string representation of the scope to parse.
///
/// Returns:
/// A list of [_AuthScopeSegment] objects representing the parsed segments of the scope.
///
/// Throws:
/// [FormatException] if the [scopeString] is empty or contains invalid segments.
static List<_AuthScopeSegment> _parseSegments(String scopeString) {
if (scopeString.isEmpty) {
throw FormatException(
@ -435,7 +853,7 @@ class AuthScope {
return elements;
}
/// Whether or not this instance is a subset or equal to [incomingScope].
/// Determines if this [AuthScope] is a subset of or equal to the [incomingScope].
///
/// The scope `users:posts` is a subset of `users`.
///
@ -477,16 +895,42 @@ class AuthScope {
}
/// Alias of [isSubsetOrEqualTo].
///
/// This method is deprecated and will be removed in a future version.
/// Use [isSubsetOrEqualTo] instead.
///
/// Determines if this [AuthScope] allows the [incomingScope].
/// It is equivalent to calling [isSubsetOrEqualTo] with the same argument.
///
/// [incomingScope]: The AuthScope to compare against this instance.
///
/// Returns true if this AuthScope is a subset of or equal to the [incomingScope],
/// false otherwise.
@Deprecated('Use AuthScope.isSubsetOrEqualTo() instead')
bool allowsScope(AuthScope incomingScope) => isSubsetOrEqualTo(incomingScope);
/// String variant of [isSubsetOrEqualTo].
/// Checks if this AuthScope is a subset of or equal to the given scope string.
///
/// Parses an instance of this type from [scopeString] and invokes
/// [isSubsetOrEqualTo].
bool allows(String scopeString) => isSubsetOrEqualTo(AuthScope(scopeString));
/// Whether or not two scopes are exactly the same.
/// Determines if this [AuthScope] is exactly the same as the given [scope].
///
/// This method compares each segment and modifier of both scopes to ensure they are identical.
///
/// Parameters:
/// [scope]: The [AuthScope] to compare against this instance.
///
/// Returns:
/// [bool]: true if both scopes are exactly the same, false otherwise.
///
/// The comparison is performed as follows:
/// 1. Iterates through each segment of both scopes simultaneously.
/// 2. If the given scope has fewer segments, returns false.
/// 3. Compares the name and modifier of each segment.
/// 4. If any segment's name or modifier doesn't match, returns false.
/// 5. If all segments match and both scopes have the same number of segments, returns true.
bool isExactlyScope(AuthScope scope) {
final incomingIterator = scope._segments.iterator;
for (final segment in _segments) {
@ -506,18 +950,44 @@ class AuthScope {
return true;
}
/// String variant of [isExactlyScope].
/// Checks if this AuthScope is exactly the same as the given scope string.
///
/// Parses an instance of this type from [scopeString] and invokes [isExactlyScope].
bool isExactly(String scopeString) {
return isExactlyScope(AuthScope(scopeString));
}
/// Returns a string representation of this AuthScope.
///
/// This method overrides the default [Object.toString] method to provide
/// a string representation of the AuthScope instance. It returns the
/// original, unparsed scope string that was used to create this AuthScope.
///
/// Returns:
/// A [String] representing the complete scope, including all segments
/// and modifiers, exactly as it was originally provided.
///
/// Example:
/// final scope = AuthScope('user:profile.readonly');
/// print(scope.toString()); // Outputs: 'user:profile.readonly'
@override
String toString() => _scopeString;
}
/// Represents a segment of an AuthScope.
///
/// An AuthScope can be composed of one or more segments, where each segment
/// may have a name and an optional modifier. This class parses and stores
/// the components of a single segment.
class _AuthScopeSegment {
/// Constructs an AuthScopeSegment from the given [segment] string.
///
/// The [segment] string is expected to be in the format "name.modifier" or "name".
/// If a modifier is present, it is stored in the [modifier] field. If not, [modifier]
/// remains null. The [name] field always contains the name of the segment.
///
/// Parameters:
/// [segment]: A [String] representing the segment, which may include a modifier.
_AuthScopeSegment(String segment) {
final split = segment.split(".");
if (split.length == 2) {
@ -528,9 +998,42 @@ class _AuthScopeSegment {
}
}
/// The name of the segment.
///
/// This property represents the main part of the segment before any modifier.
/// For example, in the segment "user.readonly", "user" would be the name.
///
/// This value can be null if the segment is empty or malformed.
String? name;
/// The modifier of the segment, if present.
///
/// This property represents the optional part of the segment after the name,
/// if a modifier is specified. For example, in the segment "user.readonly",
/// "readonly" would be the modifier.
///
/// If no modifier is present in the segment, this value is null.
String? modifier;
/// Returns a string representation of this AuthScopeSegment.
///
/// This method overrides the default [Object.toString] method to provide
/// a string representation of the AuthScopeSegment instance.
///
/// If the segment has a modifier, it returns the name and modifier
/// separated by a dot (e.g., "name.modifier").
/// If there's no modifier, it returns just the name.
///
/// Returns:
/// A [String] representing the complete segment, including the
/// modifier if present.
///
/// Example:
/// final segment = _AuthScopeSegment('user.readonly');
/// print(segment.toString()); // Outputs: 'user.readonly'
///
/// final segmentNoModifier = _AuthScopeSegment('user');
/// print(segmentNoModifier.toString()); // Outputs: 'user'
@override
String toString() {
if (modifier == null) {

View file

@ -1,7 +1,16 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'package:protevus_auth/auth.dart';
/// The properties of an OAuth 2.0 Resource Owner.
/// Defines the interface for a Resource Owner in OAuth 2.0 authentication.
///
/// Your application's 'user' type must implement the methods declared in this interface. [AuthServer] can
/// validate the credentials of a [ResourceOwner] to grant authorization codes and access tokens on behalf of that
@ -9,20 +18,59 @@ import 'package:protevus_auth/auth.dart';
abstract class ResourceOwner {
/// The username of the resource owner.
///
/// This value must be unique amongst all resource owners. It is often an email address. This value
/// is used by authenticating users to identify their account.
/// This property represents the unique identifier for a resource owner, typically used for authentication purposes.
/// It must be unique among all resource owners in the system. Often, this value is an email address.
///
/// The username is used by authenticating users to identify their account when logging in or performing
/// other authentication-related actions.
///
/// This property is nullable, which means it can be null in some cases, such as when creating a new
/// resource owner instance before setting the username.
String? username;
/// The hashed password of this instance.
///
/// This property stores the password of the resource owner in a hashed format.
/// Hashing is a one-way process that converts the plain text password into a
/// fixed-length string of characters, which is more secure to store than the
/// original password.
///
/// The hashed password is used for password verification during authentication
/// without storing the actual password. This enhances security by ensuring
/// that even if the database is compromised, the original passwords remain
/// unknown.
///
/// This property is nullable, allowing for cases where a password might not
/// be set or required for certain types of resource owners.
String? hashedPassword;
/// The salt the [hashedPassword] was hashed with.
/// The salt used in the hashing process for [hashedPassword].
///
/// A salt is a random string that is added to the password before hashing,
/// which adds an extra layer of security to the hashed password. It helps
/// protect against rainbow table attacks and ensures that even if two users
/// have the same password, their hashed passwords will be different.
///
/// This property is nullable to accommodate cases where a salt might not be
/// used or stored separately from the hashed password.
String? salt;
/// A unique identifier of this resource owner.
///
/// This unique identifier is used by [AuthServer] to associate authorization codes and access tokens with
/// this resource owner.
/// This property represents a unique identifier for the resource owner, typically
/// used in authentication and authorization processes. The [AuthServer] uses this
/// identifier to associate authorization codes and access tokens with the specific
/// resource owner.
///
/// The identifier is of type [int] and is nullable, allowing for cases where an ID
/// might not be assigned yet (e.g., when creating a new resource owner instance).
///
/// It's crucial to ensure that this ID remains unique across all resource owners
/// in the system to maintain the integrity of the authentication and authorization
/// processes.
///
/// This getter method should be implemented to return the unique identifier of
/// the resource owner.
int? get id;
}
@ -33,8 +81,23 @@ abstract class ResourceOwner {
///
/// Prefer to use `ManagedAuthDelegate` from 'package:conduit_core/managed_auth.dart' instead of implementing this interface;
/// there are important details to consider and test when implementing this interface.
///
/// This abstract class defines the contract for implementing an authentication and authorization system.
/// It provides methods for managing resource owners, clients, tokens, and authorization codes.
/// Implementations of this class are responsible for handling the storage, retrieval, and management
/// of these entities, as well as customizing certain behaviors of the authentication process.
///
/// Key responsibilities include:
/// - Managing resource owners (users)
/// - Handling client applications
/// - Storing and retrieving access and refresh tokens
/// - Managing authorization codes
/// - Customizing token formats and allowed scopes
///
/// Each method in this class corresponds to a specific operation in the OAuth 2.0 flow,
/// allowing for a flexible and extensible authentication system.
abstract class AuthServerDelegate {
/// Must return a [ResourceOwner] for a [username].
/// Retrieves a [ResourceOwner] based on the provided [username].
///
/// This method must return an instance of [ResourceOwner] if one exists for [username]. Otherwise, it must return null.
///
@ -43,19 +106,19 @@ abstract class AuthServerDelegate {
/// [server] is the [AuthServer] invoking this method.
FutureOr<ResourceOwner?> getResourceOwner(AuthServer server, String username);
/// Must store [client].
/// Stores a new [AuthClient] in the system.
///
/// [client] must be returned by [getClient] after this method has been invoked, and until (if ever)
/// [removeClient] is invoked.
FutureOr addClient(AuthServer server, AuthClient client);
/// Must return [AuthClient] for a client ID.
/// Retrieves an [AuthClient] based on the provided client ID.
///
/// This method must return an instance of [AuthClient] if one exists for [clientID]. Otherwise, it must return null.
/// [server] is the [AuthServer] requesting the [AuthClient].
FutureOr<AuthClient?> getClient(AuthServer server, String clientID);
/// Removes an [AuthClient] for a client ID.
/// Removes an [AuthClient] for a given client ID.
///
/// This method must delete the [AuthClient] for [clientID]. Subsequent requests to this
/// instance for [getClient] must return null after this method completes. If there is no
@ -64,7 +127,7 @@ abstract class AuthServerDelegate {
/// [server] is the [AuthServer] requesting the [AuthClient].
FutureOr removeClient(AuthServer server, String clientID);
/// Returns a [AuthToken] searching by its access token or refresh token.
/// Retrieves an [AuthToken] based on either its access token or refresh token.
///
/// Exactly one of [byAccessToken] and [byRefreshToken] may be non-null, if not, this method must throw an error.
///
@ -80,12 +143,12 @@ abstract class AuthServerDelegate {
String? byRefreshToken,
});
/// This method must delete all [AuthToken] and [AuthCode]s for a [ResourceOwner].
/// Deletes all [AuthToken]s and [AuthCode]s associated with a specific [ResourceOwner].
///
/// [server] is the requesting [AuthServer]. [resourceOwnerID] is the [ResourceOwner.id].
FutureOr removeTokens(AuthServer server, int resourceOwnerID);
/// Must delete a [AuthToken] granted by [grantedByCode].
/// Deletes an [AuthToken] that was granted by a specific [AuthCode].
///
/// If an [AuthToken] has been granted by exchanging [AuthCode], that token must be revoked
/// and can no longer be used to authorize access to a resource. [grantedByCode] should
@ -94,7 +157,7 @@ abstract class AuthServerDelegate {
/// This method is invoked when attempting to exchange an authorization code that has already granted a token.
FutureOr removeToken(AuthServer server, AuthCode grantedByCode);
/// Must store [token].
/// Stores an [AuthToken] in the system.
///
/// [token] must be stored such that it is accessible from [getToken], and until it is either
/// revoked via [removeToken] or [removeTokens], or until it has expired and can reasonably
@ -107,7 +170,7 @@ abstract class AuthServerDelegate {
/// is null.
FutureOr addToken(AuthServer server, AuthToken token, {AuthCode? issuedFrom});
/// Must update [AuthToken] with [newAccessToken, [newIssueDate, [newExpirationDate].
/// Updates an existing [AuthToken] with new values.
///
/// This method must must update an existing [AuthToken], found by [oldAccessToken],
/// with the values [newAccessToken], [newIssueDate] and [newExpirationDate].
@ -122,23 +185,23 @@ abstract class AuthServerDelegate {
DateTime? newExpirationDate,
);
/// Must store [code].
/// Stores an [AuthCode] in the system.
///
/// [code] must be accessible until its expiration date.
FutureOr addCode(AuthServer server, AuthCode code);
/// Must return [AuthCode] for its identifiying [code].
/// Retrieves an [AuthCode] based on its identifying code.
///
/// This must return an instance of [AuthCode] where [AuthCode.code] matches [code].
/// Return null if no matching code.
FutureOr<AuthCode?> getCode(AuthServer server, String code);
/// Must remove [AuthCode] identified by [code].
/// Removes an [AuthCode] from the system based on its identifying code.
///
/// The [AuthCode.code] matching [code] must be deleted and no longer accessible.
FutureOr removeCode(AuthServer server, String? code);
/// Returns list of allowed scopes for a given [ResourceOwner].
/// Returns a list of allowed scopes for a given [ResourceOwner].
///
/// Subclasses override this method to return a list of [AuthScope]s based on some attribute(s) of an [ResourceOwner].
/// That [ResourceOwner] is then restricted to only those scopes, even if the authenticating client would allow other scopes

View file

@ -1,3 +1,12 @@
/*
* This file is part of the Protevus Platform.
*
* (C) Protevus <developers@protevus.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import 'dart:async';
import 'package:protevus_openapi/documentable.dart';
@ -5,14 +14,14 @@ import 'package:protevus_auth/auth.dart';
import 'package:protevus_http/http.dart';
import 'package:protevus_openapi/v3.dart';
/// Instances that implement this type can be used by an [Authorizer] to determine authorization of a request.
/// A mixin that defines the interface for validating authorization data.
///
/// When an [Authorizer] processes a [Request], it invokes [validate], passing in the parsed Authorization
/// header of the [Request].
///
/// [AuthServer] implements this interface.
mixin AuthValidator {
/// Returns an [Authorization] if [authorizationData] is valid.
/// Validates authorization data and returns an [Authorization] if valid.
///
/// This method is invoked by [Authorizer] to validate the Authorization header of a request. [authorizationData]
/// is the parsed contents of the Authorization header, while [parser] is the object that parsed the header.
@ -28,7 +37,7 @@ mixin AuthValidator {
List<AuthScope>? requiredScope,
});
/// Provide [APISecurityRequirement]s for [authorizer].
/// Provides [APISecurityRequirement]s for the given [authorizer].
///
/// An [Authorizer] that adds security requirements to operations will invoke this method to allow this validator to define those requirements.
/// The [Authorizer] must provide the [context] it was given to document the operations, itself and optionally a list of [scopes] required to pass it.