From 2cb685578b957772dc87f281142b42d763528ea8 Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Fri, 6 Sep 2024 11:52:59 -0700 Subject: [PATCH] update: updating files with detailed comments --- packages/auth/lib/auth.dart | 62 +- packages/auth/lib/src/auth.dart | 70 -- .../auth/lib/src/auth_code_controller.dart | 229 ++++++- packages/auth/lib/src/auth_controller.dart | 233 +++++++ .../lib/src/auth_redirect_controller.dart | 235 ++++++- .../auth/lib/src/authorization_parser.dart | 130 +++- .../auth/lib/src/authorization_server.dart | 392 +++++++++++- packages/auth/lib/src/authorizer.dart | 166 ++++- packages/auth/lib/src/exceptions.dart | 153 ++++- packages/auth/lib/src/objects.dart | 605 ++++++++++++++++-- packages/auth/lib/src/protocols.dart | 101 ++- packages/auth/lib/src/validator.dart | 15 +- 12 files changed, 2153 insertions(+), 238 deletions(-) delete mode 100644 packages/auth/lib/src/auth.dart diff --git a/packages/auth/lib/auth.dart b/packages/auth/lib/auth.dart index 0da5fe6..8736392 100644 --- a/packages/auth/lib/auth.dart +++ b/packages/auth/lib/auth.dart @@ -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); +} diff --git a/packages/auth/lib/src/auth.dart b/packages/auth/lib/src/auth.dart deleted file mode 100644 index 57f2c62..0000000 --- a/packages/auth/lib/src/auth.dart +++ /dev/null @@ -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); -} diff --git a/packages/auth/lib/src/auth_code_controller.dart b/packages/auth/lib/src/auth_code_controller.dart index 12bacb2..087f5e2 100644 --- a/packages/auth/lib/src/auth_code_controller.dart +++ b/packages/auth/lib/src/auth_code_controller.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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 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 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 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 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, { diff --git a/packages/auth/lib/src/auth_controller.dart b/packages/auth/lib/src/auth_controller.dart index 776aae1..050f6bd 100644 --- a/packages/auth/lib/src/auth_controller.dart +++ b/packages/auth/lib/src/auth_controller.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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 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=" 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=&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=&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=&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 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 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 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)}, diff --git a/packages/auth/lib/src/auth_redirect_controller.dart b/packages/auth/lib/src/auth_redirect_controller.dart index f1234d9..7413bdd 100644 --- a/packages/auth/lib/src/auth_redirect_controller.dart +++ b/packages/auth/lib/src/auth_redirect_controller.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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: "

Error

unsupported_response_type

", )..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 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 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 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, { diff --git a/packages/auth/lib/src/authorization_parser.dart b/packages/auth/lib/src/authorization_parser.dart index 06e318a..885ae45 100644 --- a/packages/auth/lib/src/authorization_parser.dart +++ b/packages/auth/lib/src/authorization_parser.dart @@ -1,5 +1,23 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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 { const AuthorizationParser(); @@ -7,11 +25,24 @@ abstract class AuthorizationParser { } /// 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 { 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 { /// 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 ", +/// 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 { + /// 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); diff --git a/packages/auth/lib/src/authorization_server.dart b/packages/auth/lib/src/authorization_server.dart index 6da3cfc..269ec8b 100644 --- a/packages/auth/lib/src/authorization_server.dart +++ b/packages/auth/lib/src/authorization_server.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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 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 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 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 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 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] 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 validate( AuthorizationParser 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] 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 _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? _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"; diff --git a/packages/auth/lib/src/authorizer.dart b/packages/auth/lib/src/authorizer.dart index 7b50145..832a3dc 100644 --- a/packages/auth/lib/src/authorizer.dart +++ b/packages/auth/lib/src/authorizer.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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? 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 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 documentOperations( APIDocumentContext context, diff --git a/packages/auth/lib/src/exceptions.dart b/packages/auth/lib/src/exceptions.dart index cabab92..40168b0 100644 --- a/packages/auth/lib/src/exceptions.dart +++ b/packages/auth/lib/src/exceptions.dart @@ -1,11 +1,58 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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 } diff --git a/packages/auth/lib/src/objects.dart b/packages/auth/lib/src/objects.dart index b49a2d0..74ab98a 100644 --- a/packages/auth/lib/src/objects.dart +++ b/packages/auth/lib/src/objects.dart @@ -1,20 +1,32 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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? 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? _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 ". + /// + /// 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 + /// + /// 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? 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? 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? 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 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 _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 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) { diff --git a/packages/auth/lib/src/protocols.dart b/packages/auth/lib/src/protocols.dart index e61451c..3a58a41 100644 --- a/packages/auth/lib/src/protocols.dart +++ b/packages/auth/lib/src/protocols.dart @@ -1,7 +1,16 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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 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 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 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 diff --git a/packages/auth/lib/src/validator.dart b/packages/auth/lib/src/validator.dart index 2a89ea5..0582d4b 100644 --- a/packages/auth/lib/src/validator.dart +++ b/packages/auth/lib/src/validator.dart @@ -1,3 +1,12 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * 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? 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.