diff --git a/Sources/AuthFoundation/AuthFoundation.docc/WorkingWithClaims.md b/Sources/AuthFoundation/AuthFoundation.docc/WorkingWithClaims.md new file mode 100644 index 000000000..9f5ae3d73 --- /dev/null +++ b/Sources/AuthFoundation/AuthFoundation.docc/WorkingWithClaims.md @@ -0,0 +1,76 @@ +# Working with Claims + +Using Claims on the various types included in OIDC and AuthFoundation. + +## Overview + +OpenID Connect (OIDC) uses claims to describe individual pieces of information, like email or name, packaged into responses from the server. ID Tokens in particular contain claims, packaged within a JSON Web Token (``JWT``). A variety of other OIDC capabilities also supply information using claims, such as the OpenID Configuration (``OpenIdConfiguration``) metadata returned from the server, when [introspecting a token](``Credential/introspect(_:)``). + +Since claims are a common characteristic of authentication, this SDK provides features that improves the developer experience (DX) to simplify how this information is used, and to make these capabilities consistent across areas of the toolchain. + +## Types that have claims + +A variety of types contain claims, which are identified by types that conform to the ``HasClaims`` protocol. This protocol provides common access patterns for using claims, and conveniences for simplfying how you can access them. + +Some of these types include: + +* ``JWT`` +* ``UserInfo`` +* ``TokenInfo`` +* ``OpenIdConfiguration`` +* ``Token/Metadata`` + +To better understand how these types can be used, it's best to work with an example, starting with ``JWT``. + +## Examples of using Claims + +When a user signs in they are issued a ``Token/idToken`` which is returned as a JSON Web Token ``JWT``. This token contains information about the user, and other important values which could be useful to your application. For example, you may wish to retrieve the user's name and "subject" (their user identifier) to display within your interface. The ``HasClaims`` protocol makes this easy by providing several ways to extract information. + +### Keyed Subscripting, using strings + +If you know the string identifier for the claim, you can use that as a subscript key on the relevant object. + +```swift +if let identifier = token.idToken?["sub"] { + Text("Username: \(identifier)") +} +``` + +This can be useful, especially when your application uses custom claims supplied from the authorization server, but when using standard claim values, it can be more convenient to use enums. + +### Keyed Subscripting, using claim enum values + +Enum values are often more convenient to use since code auto-completion and compile-time warnings can ensure consistency when working with these values. + +```swift +if let identifier = token.idToken?[.subject] { + Text("Username: \(identifier)") +} +``` + +When working with claims, the type of enum is defined by the conforming type, which can help give you an insight into the possible options available to you. + +### Convenience properties + +Finally, some common claims which are best represented as more concrete types, such as URL or Date, are provided to simplify your workflow. For example, if you want to retrieve the date the user was authenticated using ``HasClaims/authTime``, or the user's locale (in an Apple-friendly format) using ``HasClaims/userLocale``. + +```swift +if let authTime = token.idToken?.authTime, + authTime.timeIntervalSinceNow < 3600 { + // The user authenticated more than one hour ago +} +``` + +### Enums and arrays of converted values + +Some types conform to a special protocol called ``ClaimConvertable``, which enables concrete types to be convertable from the raw claim values supplied by the authorization server. This can make interacting with claims easier and more developer-friendly. + +One example of this type is the ``HasClaims/authenticationMethods`` property. The ``JWTClaim/authMethodsReference`` claim returns an array of the methods a user used to authenticate their account. The values for this claim can be represented by the ``AuthenticationMethod`` enum, so instead of performing string comparisons, you can reference this convenience property to work with the authentication methods reference as an array of enums. + +```swift +if let authenticationMethods = token.idToken?.authenticationMethods, + authenticationMethods.contains(.multipleFactor) +{ + // The user authenticated using some multifactor step +} +``` diff --git a/Sources/AuthFoundation/JWT/Enums/JWTClaim.swift b/Sources/AuthFoundation/JWT/Enums/JWTClaim.swift index c7c4d213a..0cabfca55 100644 --- a/Sources/AuthFoundation/JWT/Enums/JWTClaim.swift +++ b/Sources/AuthFoundation/JWT/Enums/JWTClaim.swift @@ -232,6 +232,18 @@ public enum JWTClaim: Codable, IsClaim { /// Token introspection response case tokenIntrospection + + /// Indicates whether the transaction is on a nonce-supported platform. If you sent a nonce in the authorization request but do not see the nonce claim in the ID token, check this claim to determine how to proceed. Used predominantly by Sign In With Apple. + case nonceSupported + + /// Indicates the liklihood of whether or not this appears to be a real user. Used predominantly by Sign In With Apple. + case realUserStatus + + /// Indicates if the email address provided is a proxied address. Used predominantly by Sign In With Apple. + case isPrivateEmail + + /// Identifier used when transfering subjects. Used predominantly by Sign In With Apple. + case transferSubject /// Custom claim with the given name case custom(_ name: String) diff --git a/Sources/AuthFoundation/JWT/Extensions/Claim+Extensions.swift b/Sources/AuthFoundation/JWT/Extensions/Claim+Extensions.swift index bd2bd96d8..6a998f9a4 100644 --- a/Sources/AuthFoundation/JWT/Extensions/Claim+Extensions.swift +++ b/Sources/AuthFoundation/JWT/Extensions/Claim+Extensions.swift @@ -165,6 +165,14 @@ extension JWTClaim: RawRepresentable, Equatable { self = .entitlements case "token_introspection": self = .tokenIntrospection + case "nonce_supported": + self = .nonceSupported + case "real_user_status": + self = .realUserStatus + case "is_private_email": + self = .isPrivateEmail + case "transfer_sub": + self = .transferSubject default: self = .custom(rawValue) } @@ -318,6 +326,14 @@ extension JWTClaim: RawRepresentable, Equatable { return "entitlements" case .tokenIntrospection: return "token_introspection" + case .nonceSupported: + return "nonce_supported" + case .realUserStatus: + return "real_user_status" + case .isPrivateEmail: + return "is_private_email" + case .transferSubject: + return "transfer_sub" case .custom(let name): return name } diff --git a/Sources/AuthFoundation/JWT/JWT.swift b/Sources/AuthFoundation/JWT/JWT.swift index 5e7fd3a35..64b6f7632 100644 --- a/Sources/AuthFoundation/JWT/JWT.swift +++ b/Sources/AuthFoundation/JWT/JWT.swift @@ -42,7 +42,7 @@ public struct JWT: RawRepresentable, Codable, HasClaims, Expires { /// The authentication context class reference. /// - /// The ``Claim/authContextClassReference`` claim (or `acr` in string form) defines a special authentication context reference which indicates additional policy choices requested when authenticating a user. + /// The ``JWTClaim/authContextClassReference`` claim (or `acr` in string form) defines a special authentication context reference which indicates additional policy choices requested when authenticating a user. public var authenticationContext: String? { self[.authContextClassReference] } /// JWT header information describing the contents of the token. diff --git a/Sources/AuthFoundation/Responses/OpenIdConfiguration.swift b/Sources/AuthFoundation/Responses/OpenIdConfiguration.swift index faf7df34f..ad8b85370 100644 --- a/Sources/AuthFoundation/Responses/OpenIdConfiguration.swift +++ b/Sources/AuthFoundation/Responses/OpenIdConfiguration.swift @@ -14,7 +14,7 @@ import Foundation /// Describes the configuration of an OpenID server. /// -/// The values exposed from this configuration are typically used during authentication, or when querying a server for its capabilities. +/// The values exposed from this configuration are typically used during authentication, or when querying a server for its capabilities. This type uses ``HasClaims`` to represent the various provider metadata (represented as ``OpenIdConfiguration/ProviderMetadata``) for returning the full contents of the server's configuration. For more information, please refer to the documentation. public struct OpenIdConfiguration: Codable, JSONClaimContainer { public typealias ClaimType = ProviderMetadata diff --git a/Sources/AuthFoundation/Token Management/Token.swift b/Sources/AuthFoundation/Token Management/Token.swift index b555be372..7db3d762f 100644 --- a/Sources/AuthFoundation/Token Management/Token.swift +++ b/Sources/AuthFoundation/Token Management/Token.swift @@ -45,6 +45,8 @@ public final class Token: Codable, Equatable, Hashable, Expires { public let refreshToken: String? /// The ID token, if requested. + /// + /// For more information on working with an ID token, see the documentation. public let idToken: JWT? /// Defines the context this token was issued from. diff --git a/Sources/AuthFoundation/User Management/Credential+Extensions.swift b/Sources/AuthFoundation/User Management/Credential+Extensions.swift index c0ae85a73..864752557 100644 --- a/Sources/AuthFoundation/User Management/Credential+Extensions.swift +++ b/Sources/AuthFoundation/User Management/Credential+Extensions.swift @@ -99,5 +99,16 @@ extension Credential { } } } + + /// Introspect the token to check it for validity, and read the additional information associated with it. + /// - Parameters: + /// - type: Type of token to introspect. + public func introspect(_ type: Token.Kind) async throws -> TokenInfo { + try await withCheckedThrowingContinuation { continuation in + oauth2.introspect(token: token, type: type) { result in + continuation.resume(with: result) + } + } + } } #endif diff --git a/Tests/AuthFoundationTests/OpenIDConfigurationTests.swift b/Tests/AuthFoundationTests/OpenIDConfigurationTests.swift index 7f925c401..0f77a8f46 100644 --- a/Tests/AuthFoundationTests/OpenIDConfigurationTests.swift +++ b/Tests/AuthFoundationTests/OpenIDConfigurationTests.swift @@ -215,5 +215,19 @@ final class OpenIDConfigurationTests: XCTestCase { .subject, .custom("transfer_sub"), ]) + XCTAssertEqual(config.claimsSupported, [ + .audience, + .email, + .emailVerified, + .expirationTime, + .issuedAt, + .isPrivateEmail, + .issuer, + .nonce, + .nonceSupported, + .realUserStatus, + .subject, + .transferSubject, + ]) } }