-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement RFC 8414 compliant Authorization Server Metadata endpoint (#12
) This commit introduces a new endpoint at /.well-known/oauth-authorization-server to expose OAuth 2.0 server metadata conforming to RFC 8414. The endpoint provides information about the server's configuration, including supported grant types, response types, scopes, and endpoints. This allows clients to dynamically discover the server's capabilities and adapt their interactions accordingly. The metadata endpoint implementation includes: - Required fields per RFC 8414 - Recommended fields with appropriate default values - Optional fields with configurable presence based on feature support - Custom metadata provider for overriding default values - Error handling for invalid requests The endpoint also includes proper HTTP headers to ensure cache control and content type compliance. Closes #11
- Loading branch information
Showing
10 changed files
with
500 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
106 changes: 106 additions & 0 deletions
106
Sources/VaporOAuth/DefaultImplementations/DefaultServerMetadataProvider.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import Vapor | ||
|
||
/// Default implementation that automatically derives OAuth 2.0 server metadata from configuration | ||
public struct DefaultServerMetadataProvider: ServerMetadataProvider { | ||
private let issuer: String | ||
private let validScopes: [String]? | ||
private let clientRetriever: ClientRetriever | ||
private let hasCodeManager: Bool | ||
private let hasDeviceCodeManager: Bool | ||
private let hasTokenIntrospection: Bool | ||
private let hasUserManager: Bool | ||
private let jwksEndpoint: String | ||
|
||
/// Initialize the metadata provider with OAuth 2.0 server configuration | ||
/// - Parameters: | ||
/// - issuer: The issuer identifier for the OAuth 2.0 authorization server | ||
/// - validScopes: List of supported OAuth scopes, if any | ||
/// - clientRetriever: Service for retrieving OAuth client information | ||
/// - hasCodeManager: Whether authorization code flow is supported | ||
/// - hasDeviceCodeManager: Whether device authorization flow is supported | ||
/// - hasTokenIntrospection: Whether token introspection is supported | ||
/// - hasUserManager: Whether resource owner password credentials flow is supported | ||
/// - jwksEndpoint: Optional custom JWKS endpoint URL. If nil, defaults to /.well-known/jwks.json | ||
init( | ||
issuer: String, | ||
validScopes: [String]?, | ||
clientRetriever: ClientRetriever, | ||
hasCodeManager: Bool, | ||
hasDeviceCodeManager: Bool, | ||
hasTokenIntrospection: Bool, | ||
hasUserManager: Bool, | ||
jwksEndpoint: String? = nil | ||
) { | ||
self.issuer = issuer | ||
self.validScopes = validScopes | ||
self.clientRetriever = clientRetriever | ||
self.hasCodeManager = hasCodeManager | ||
self.hasDeviceCodeManager = hasDeviceCodeManager | ||
self.hasTokenIntrospection = hasTokenIntrospection | ||
self.hasUserManager = hasUserManager | ||
|
||
let baseURL = issuer.hasSuffix("/") ? String(issuer.dropLast()) : issuer | ||
self.jwksEndpoint = jwksEndpoint ?? "\(baseURL)/.well-known/jwks.json" | ||
} | ||
|
||
public func getMetadata() async throws -> OAuthServerMetadata { | ||
let baseURL = issuer.hasSuffix("/") ? String(issuer.dropLast()) : issuer | ||
|
||
// Build list of supported grant types based on configuration | ||
var supportedGrantTypes = [ | ||
OAuthFlowType.clientCredentials.rawValue, | ||
OAuthFlowType.refresh.rawValue | ||
] | ||
|
||
if hasCodeManager { | ||
supportedGrantTypes.append(OAuthFlowType.authorization.rawValue) | ||
} | ||
|
||
if hasDeviceCodeManager { | ||
supportedGrantTypes.append(OAuthFlowType.deviceCode.rawValue) | ||
} | ||
|
||
if hasUserManager { | ||
supportedGrantTypes.append(OAuthFlowType.password.rawValue) | ||
} | ||
|
||
// Configure supported response types per OAuth 2.0 spec | ||
var responseTypes = ["code"] | ||
if hasCodeManager { | ||
responseTypes.append("token") | ||
} | ||
|
||
return OAuthServerMetadata( | ||
// Required metadata fields per RFC 8414 | ||
issuer: issuer, | ||
authorizationEndpoint: "\(baseURL)/oauth/authorize", | ||
tokenEndpoint: "\(baseURL)/oauth/token", | ||
jwksUri: jwksEndpoint, | ||
responseTypesSupported: responseTypes, | ||
subjectTypesSupported: ["public"], | ||
idTokenSigningAlgValuesSupported: ["RS256"], | ||
|
||
// Recommended metadata fields | ||
scopesSupported: validScopes, | ||
tokenEndpointAuthMethodsSupported: ["client_secret_basic", "client_secret_post"], | ||
grantTypesSupported: supportedGrantTypes, | ||
userinfoEndpoint: nil, | ||
registrationEndpoint: nil, | ||
claimsSupported: nil, | ||
|
||
// Optional metadata fields | ||
tokenIntrospectionEndpoint: hasTokenIntrospection ? "\(baseURL)/oauth/token_info" : nil, | ||
tokenRevocationEndpoint: "\(baseURL)/oauth/revoke", | ||
serviceDocumentation: nil, | ||
uiLocalesSupported: nil, | ||
opPolicyUri: nil, | ||
opTosUri: nil, | ||
revocationEndpointAuthMethodsSupported: ["client_secret_basic", "client_secret_post"], | ||
revocationEndpointAuthSigningAlgValuesSupported: nil, | ||
introspectionEndpointAuthMethodsSupported: hasTokenIntrospection ? ["client_secret_basic"] : nil, | ||
introspectionEndpointAuthSigningAlgValuesSupported: nil, | ||
codeChallengeMethodsSupported: hasCodeManager ? ["S256", "plain"] : nil, | ||
deviceAuthorizationEndpoint: hasDeviceCodeManager ? "\(baseURL)/oauth/device_authorization" : nil | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import Vapor | ||
|
||
/// RFC 8414 compliant Authorization Server Metadata | ||
/// https://datatracker.ietf.org/doc/html/rfc8414#section-2 | ||
public struct OAuthServerMetadata: Content, Sendable { | ||
// Required fields per RFC 8414 | ||
let issuer: String | ||
let authorizationEndpoint: String | ||
let tokenEndpoint: String | ||
let jwksUri: String | ||
let responseTypesSupported: [String] | ||
let subjectTypesSupported: [String] | ||
let idTokenSigningAlgValuesSupported: [String] | ||
|
||
// Recommended fields | ||
let scopesSupported: [String]? | ||
let tokenEndpointAuthMethodsSupported: [String]? | ||
let grantTypesSupported: [String]? | ||
let userinfoEndpoint: String? | ||
let registrationEndpoint: String? | ||
let claimsSupported: [String]? | ||
|
||
// Optional fields | ||
let tokenIntrospectionEndpoint: String? | ||
let tokenRevocationEndpoint: String? | ||
let serviceDocumentation: String? | ||
let uiLocalesSupported: [String]? | ||
let opPolicyUri: String? | ||
let opTosUri: String? | ||
let revocationEndpointAuthMethodsSupported: [String]? | ||
let revocationEndpointAuthSigningAlgValuesSupported: [String]? | ||
let introspectionEndpointAuthMethodsSupported: [String]? | ||
let introspectionEndpointAuthSigningAlgValuesSupported: [String]? | ||
let codeChallengeMethodsSupported: [String]? | ||
let deviceAuthorizationEndpoint: String? | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case issuer | ||
case authorizationEndpoint = "authorization_endpoint" | ||
case tokenEndpoint = "token_endpoint" | ||
case jwksUri = "jwks_uri" | ||
case responseTypesSupported = "response_types_supported" | ||
case subjectTypesSupported = "subject_types_supported" | ||
case idTokenSigningAlgValuesSupported = "id_token_signing_alg_values_supported" | ||
case scopesSupported = "scopes_supported" | ||
case tokenEndpointAuthMethodsSupported = "token_endpoint_auth_methods_supported" | ||
case grantTypesSupported = "grant_types_supported" | ||
case userinfoEndpoint = "userinfo_endpoint" | ||
case registrationEndpoint = "registration_endpoint" | ||
case claimsSupported = "claims_supported" | ||
case tokenIntrospectionEndpoint = "introspection_endpoint" | ||
case tokenRevocationEndpoint = "revocation_endpoint" | ||
case serviceDocumentation = "service_documentation" | ||
case uiLocalesSupported = "ui_locales_supported" | ||
case opPolicyUri = "op_policy_uri" | ||
case opTosUri = "op_tos_uri" | ||
case revocationEndpointAuthMethodsSupported = "revocation_endpoint_auth_methods_supported" | ||
case revocationEndpointAuthSigningAlgValuesSupported = "revocation_endpoint_auth_signing_alg_values_supported" | ||
case introspectionEndpointAuthMethodsSupported = "introspection_endpoint_auth_methods_supported" | ||
case introspectionEndpointAuthSigningAlgValuesSupported = "introspection_endpoint_auth_signing_alg_values_supported" | ||
case codeChallengeMethodsSupported = "code_challenge_methods_supported" | ||
case deviceAuthorizationEndpoint = "device_authorization_endpoint" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import Vapor | ||
|
||
public protocol ServerMetadataProvider: Sendable { | ||
func getMetadata() async throws -> OAuthServerMetadata | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import Vapor | ||
import NIOHTTP1 | ||
|
||
struct MetadataHandler: Sendable { | ||
let metadataProvider: ServerMetadataProvider | ||
|
||
@Sendable | ||
func handleRequest(request: Request) async throws -> Response { | ||
let metadata = try await metadataProvider.getMetadata() | ||
return try createMetadataResponse(metadata: metadata) | ||
} | ||
|
||
private func createMetadataResponse(metadata: OAuthServerMetadata) throws -> Response { | ||
let response = Response(status: .ok) | ||
try response.content.encode(metadata) | ||
|
||
// Set required headers per RFC 8414 Section 3 | ||
response.headers.contentType = .json | ||
// Set all cache control directives explicitly | ||
response.headers.replaceOrAdd( | ||
name: .cacheControl, | ||
value: "no-store, no-cache, max-age=0, must-revalidate" | ||
) | ||
response.headers.replaceOrAdd(name: .pragma, value: "no-cache") | ||
|
||
return response | ||
} | ||
|
||
private func createErrorResponse( | ||
status: HTTPStatus, | ||
errorMessage: String, | ||
errorDescription: String | ||
) throws -> Response { | ||
let response = Response(status: status) | ||
try response.content.encode(ErrorResponse( | ||
error: errorMessage, | ||
errorDescription: errorDescription | ||
)) | ||
return response | ||
} | ||
} | ||
|
||
extension MetadataHandler { | ||
struct ErrorResponse: Content { | ||
let error: String | ||
let errorDescription: String | ||
|
||
enum CodingKeys: String, CodingKey { | ||
case error | ||
case errorDescription = "error_description" | ||
} | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
Tests/VaporOAuthTests/Fakes/FakeAuthorizationHandler.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import Vapor | ||
@testable import VaporOAuth | ||
|
||
final class FakeAuthorizationHandler: AuthorizeHandler, @unchecked Sendable { | ||
var capturedRequest: Request? | ||
var capturedAuthorizationRequestObject: AuthorizationRequestObject? | ||
var shouldAuthorize: Bool | ||
|
||
init(shouldAuthorize: Bool = true) { | ||
self.shouldAuthorize = shouldAuthorize | ||
} | ||
|
||
func handleAuthorizationRequest( | ||
_ request: Request, | ||
authorizationRequestObject: AuthorizationRequestObject | ||
) async throws -> Response { | ||
capturedRequest = request | ||
capturedAuthorizationRequestObject = authorizationRequestObject | ||
|
||
let response = Response(status: shouldAuthorize ? .ok : .unauthorized) | ||
return response | ||
} | ||
|
||
func handleAuthorizationError(_ errorType: AuthorizationError) async throws -> Response { | ||
return Response(status: .badRequest) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.