Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RFC-compliant documentation to OAuth2 models #16

Merged
merged 1 commit into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions Sources/VaporOAuth/Models/OAuthClient.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,55 @@
import Vapor

/// An OAuth 2.0 client application registered with the authorization server
///
/// OAuth clients are applications that request access to protected resources on behalf of the resource owner.
/// As defined in [RFC 6749 Section 2](https://datatracker.ietf.org/doc/html/rfc6749#section-2), clients can be:
/// - Confidential: Capable of maintaining confidentiality of credentials
/// - Public: Cannot maintain credential confidentiality
///
/// Clients must register with the authorization server and be issued client credentials
/// (client ID and optionally client secret) as described in [RFC 6749 Section 2.3](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3).
public final class OAuthClient: Extendable, @unchecked Sendable {

/// The client identifier issued to the client during registration
public let clientID: String

/// List of allowed redirect URIs for authorization requests
///
/// As specified in [RFC 6749 Section 3.1.2](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2),
/// the authorization server must validate that redirect URIs in authorization requests
/// match one of the pre-registered URIs.
public let redirectURIs: [String]?

/// The client secret issued to confidential clients
///
/// As defined in [RFC 6749 Section 2.3.1](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1),
/// confidential clients are issued client credentials including a client secret
/// for authenticating with the authorization server.
public let clientSecret: String?

/// List of OAuth scopes this client is allowed to request
///
/// Scopes represent access privileges as defined in [RFC 6749 Section 3.3](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3).
/// The authorization server should validate requested scopes against this list.
public let validScopes: [String]?

/// Whether this is a confidential client that can securely store credentials
///
/// As specified in [RFC 6749 Section 2.1](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1),
/// confidential clients can maintain the confidentiality of their credentials.
public let confidentialClient: Bool?

/// Whether this is a first-party client application
///
/// First-party clients are typically applications created by the same organization
/// as the authorization server and may receive special handling.
public let firstParty: Bool

/// The OAuth flow type this client is allowed to use
///
/// Restricts which grant type the client can use to obtain access tokens
/// as defined in [RFC 6749 Section 1.3](https://datatracker.ietf.org/doc/html/rfc6749#section-1.3).
public let allowedGrantType: OAuthFlowType

public var extend: Vapor.Extend = .init()
Expand All @@ -23,6 +65,13 @@ public final class OAuthClient: Extendable, @unchecked Sendable {
self.allowedGrantType = allowedGrantType
}

/// Validates if a redirect URI matches one of the pre-registered URIs for this client
///
/// As required by [RFC 6749 Section 3.1.2.3](https://datatracker.ietf.org/doc/html/rfc6749#section-3.1.2.3),
/// the authorization server must ensure the redirect URI in authorization requests
/// exactly matches one of the pre-registered redirect URIs.
/// - Parameter redirectURI: The redirect URI to validate
/// - Returns: Whether the URI is valid for this client
func validateRedirectURI(_ redirectURI: String) -> Bool {
guard let redirectURIs = redirectURIs else {
return false
Expand Down
41 changes: 41 additions & 0 deletions Sources/VaporOAuth/Models/OAuthCode.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,57 @@
import Foundation

/// An OAuth 2.0 authorization code issued during the authorization code grant flow
///
/// Authorization codes are short-lived credentials issued to the client by the authorization server
/// after the resource owner grants authorization. As defined in [RFC 6749 Section 4.1.2](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2),
/// the code is bound to:
/// - The client identifier
/// - The redirect URI
/// - The resource owner's authorization
/// - The requested scope
///
/// The code can then be exchanged for an access token as described in [RFC 6749 Section 4.1.3](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3).
///
/// For PKCE support as specified in [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636),
/// the code may also be bound to a code challenge and challenge method.
public final class OAuthCode: @unchecked Sendable {
/// Unique identifier for this authorization code
public let codeID: String

/// The client identifier the code was issued to
public let clientID: String

/// The redirect URI specified in the authorization request
public let redirectURI: String

/// Identifier of the resource owner who granted authorization
public let userID: String

/// When this authorization code expires
public let expiryDate: Date

/// The scope of access authorized by the resource owner
public let scopes: [String]?

/// The PKCE code challenge provided in the authorization request
public let codeChallenge: String?

/// The PKCE code challenge method (e.g. "S256" or "plain")
public let codeChallengeMethod: String?

/// Storage for custom extensions
public var extend: [String: Any] = [:]

/// Initialize a new authorization code
/// - Parameters:
/// - codeID: Unique identifier for this authorization code
/// - clientID: The client identifier the code was issued to
/// - redirectURI: The redirect URI specified in the authorization request
/// - userID: Identifier of the resource owner who granted authorization
/// - expiryDate: When this authorization code expires
/// - scopes: The scope of access authorized by the resource owner
/// - codeChallenge: The PKCE code challenge provided in the authorization request
/// - codeChallengeMethod: The PKCE code challenge method (e.g. "S256" or "plain")
public init(
codeID: String,
clientID: String,
Expand Down
60 changes: 60 additions & 0 deletions Sources/VaporOAuth/Models/OAuthDeviceCode.swift
Original file line number Diff line number Diff line change
@@ -1,23 +1,75 @@
import Foundation

/// A device authorization grant issued during the OAuth 2.0 device authorization flow
///
/// Device authorization grants are used to obtain access tokens for devices with limited input capabilities
/// as defined in [RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628). The flow involves:
///
/// 1. The client requests device and user codes from the authorization server
/// 2. The authorization server issues a device code and user code
/// 3. The user enters the user code at a verification URI to authorize the device
/// 4. The client polls the token endpoint with the device code to obtain an access token
///
/// The device code response includes:
/// - A device code used to poll for authorization status
/// - A user code to be entered by the user
/// - A verification URI where the user enters the code
/// - An optional complete verification URI including the user code
/// - The lifetime of the codes
/// - The minimum interval between polling requests
public final class OAuthDeviceCode: @unchecked Sendable {
/// The device verification code
public let deviceCode: String

/// The user verification code that should be displayed to the user
public let userCode: String

/// The client identifier the device code was issued to
public let clientID: String

/// The verification URI where the user should enter the user code
public let verificationURI: String

/// Optional verification URI that includes the user code
public let verificationURIComplete: String?

/// When this device code expires
public let expiryDate: Date

/// The minimum number of seconds between polling requests
public let interval: Int

/// The scope of access requested by the client
public let scopes: [String]?

/// The current status of this device authorization grant
public var status: DeviceCodeStatus

/// Identifier of the resource owner who authorized the device, if any
public var userID: String?

/// When the device code was last used to poll for an access token
public let lastPolled: Date?

/// Whether the client should slow down polling based on the minimum interval
public var shouldIncreasePollInterval: Bool {
guard let lastPolled = lastPolled else { return false }
return Date().timeIntervalSince(lastPolled) < Double(interval)
}

/// Initialize a new device authorization grant
/// - Parameters:
/// - deviceCode: The device verification code
/// - userCode: The user verification code
/// - clientID: The client identifier the code was issued to
/// - verificationURI: The verification URI for user code entry
/// - verificationURIComplete: Optional verification URI including the user code
/// - expiryDate: When this device code expires
/// - interval: The minimum polling interval in seconds
/// - scopes: The scope of access requested
/// - status: The current authorization status
/// - userID: Identifier of the authorizing user
/// - lastPolled: When the code was last used to poll for a token
public init(
deviceCode: String,
userCode: String,
Expand Down Expand Up @@ -45,6 +97,14 @@ public final class OAuthDeviceCode: @unchecked Sendable {
}
}

/// The status of a device authorization grant
///
/// As defined in [RFC 8628 Section 3.3](https://datatracker.ietf.org/doc/html/rfc8628#section-3.3),
/// device code status can be:
/// - Pending: Waiting for user authorization
/// - Authorized: User has approved the device
/// - Unauthorized: User has not yet approved or denied
/// - Declined: User has denied authorization
public enum DeviceCodeStatus {
case pending
case authorized
Expand Down
20 changes: 20 additions & 0 deletions Sources/VaporOAuth/Models/OAuthResourceServer.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import Vapor

/// A resource server that can validate OAuth 2.0 access tokens
///
/// Resource servers are OAuth 2.0 protected API endpoints that accept and validate access tokens.
/// They authenticate to the authorization server using client credentials to perform token introspection.
///
/// As defined in [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662), resource servers
/// can use the token introspection endpoint to determine the state and validity of an access token
/// before allowing access to protected resources.
///
/// The resource server authenticates to the authorization server using HTTP Basic authentication
/// with a username and password (client credentials).
public final class OAuthResourceServer: Extendable, @unchecked Sendable {
/// The client ID used to authenticate with the authorization server
public let username: String

/// The client secret used to authenticate with the authorization server
public let password: String

/// Storage for custom extensions
public var extend: Vapor.Extend = .init()

/// Initialize a new resource server with client credentials
/// - Parameters:
/// - username: The client ID for authenticating to the authorization server
/// - password: The client secret for authenticating to the authorization server
public init(username: String, password: String) {
self.username = username
self.password = password
Expand Down
28 changes: 28 additions & 0 deletions Sources/VaporOAuth/Models/OAuthUser.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
import Vapor

/// A user account in the OAuth 2.0 authorization server
///
/// This represents a user account that can authenticate with the authorization server.
/// While related to the concept of a resource owner in [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749),
/// this model specifically represents the user's credentials and profile information stored by
/// the authorization server, rather than their role as a resource owner.
///
/// The user account enables:
/// - Authentication during the authorization process
/// - Storage of user profile information
/// - Association with granted authorizations and tokens
///
/// This separation of concerns allows the authorization server to manage user accounts
/// independently of their role in OAuth flows.
public final class OAuthUser: Authenticatable, Extendable, Encodable, @unchecked Sendable {
/// The username used to identify this user
public let username: String

/// The email address associated with this user account, if any
public let emailAddress: String?

/// The password used to authenticate this user
public var password: String

/// A unique identifier for this user account
// swiftlint:disable:next identifier_name
public var id: String?

/// Storage for custom extensions
public var extend: Extend = .init()

/// Initialize a new user account
/// - Parameters:
/// - userID: A unique identifier for this user account
/// - username: The username used to identify this user
/// - emailAddress: The email address associated with this user account
/// - password: The password used to authenticate this user
public init(userID: String? = nil, username: String, emailAddress: String?, password: String) {
self.username = username
self.emailAddress = emailAddress
Expand Down
35 changes: 35 additions & 0 deletions Sources/VaporOAuth/OAuth2.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
import Vapor

/// An OAuth 2.0 authorization server implementation
///
/// This type provides a complete OAuth 2.0 authorization server implementation following these RFCs:
///
/// - [RFC 6749: The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749)
/// - [RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://datatracker.ietf.org/doc/html/rfc6750)
/// - [RFC 7636: Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636)
/// - [RFC 7662: OAuth 2.0 Token Introspection](https://datatracker.ietf.org/doc/html/rfc7662)
/// - [RFC 8414: OAuth 2.0 Authorization Server Metadata](https://datatracker.ietf.org/doc/html/rfc8414)
/// - [RFC 8628: OAuth 2.0 Device Authorization Grant](https://datatracker.ietf.org/doc/html/rfc8628)
///
/// The server supports the following grant types:
/// - Authorization Code Grant
/// - Client Credentials Grant
/// - Resource Owner Password Credentials Grant
/// - Refresh Token Grant
/// - Device Authorization Grant
///
/// Additional features include:
/// - Token introspection for resource servers
/// - Token revocation
/// - PKCE support for public clients
/// - Server metadata discovery
public struct OAuth2: LifecycleHandler {
let codeManager: any CodeManager
let tokenManager: any TokenManager
Expand All @@ -12,6 +35,18 @@ public struct OAuth2: LifecycleHandler {
let oAuthHelper: OAuthHelper
let metadataProvider: any ServerMetadataProvider

/// Initialize a new OAuth 2.0 authorization server
/// - Parameters:
/// - codeManager: Service for managing authorization codes
/// - tokenManager: Service for managing access and refresh tokens
/// - deviceCodeManager: Service for managing device authorization grants
/// - clientRetriever: Service for retrieving registered OAuth clients
/// - authorizeHandler: Handler for the authorization endpoint UI
/// - userManager: Service for managing resource owners
/// - validScopes: List of valid OAuth scopes supported by the server
/// - resourceServerRetriever: Service for retrieving resource server credentials
/// - oAuthHelper: Helper service for OAuth operations
/// - metadataProvider: Service for providing server metadata
public init(
codeManager: CodeManager = EmptyCodeManager(),
tokenManager: TokenManager,
Expand Down
Loading