Skip to content

Commit

Permalink
Make OpenIdConfiguration adaptable to a variable number of claims, wh…
Browse files Browse the repository at this point in the history
…ile making claims themselves generic (#182)

* Make OpenIdConfiguration adaptable to a variable number of claims, while making claims themselves generic
* Lint and test updates
* Ensure NS* underlying types conform to ClaimConvertable as well
* Added / updated docs, and some missing functions
  • Loading branch information
mikenachbaur-okta authored Mar 28, 2024
1 parent 13d94e6 commit 99d57ce
Show file tree
Hide file tree
Showing 26 changed files with 720 additions and 137 deletions.
15 changes: 13 additions & 2 deletions Sources/AuthFoundation/AuthFoundation.docc/AuthFoundation.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ You can use AuthFoundation when you want to:
- ``OAuth2Client``
- ``OAuth2ClientDelegate``
- ``OpenIdConfiguration``
- ``AuthenticationMethod``
- ``AuthenticationFlow``
- ``AuthenticationDelegate``
- ``OAuth2TokenRequest``
Expand All @@ -41,14 +42,18 @@ You can use AuthFoundation when you want to:
- ``JWT``
- ``JWK``
- ``JWKS``
- ``Claim``
- ``JWTClaim``
- ``HasClaims``
- ``ClaimContainer``
- ``JSONClaimContainer``
- ``ClaimConvertable``
- ``IsClaim``
- ``Expires``

### Security

- ``Keychain``
- ``KeychainAuthenticationContext``
- ``TokenAuthenticationContext``

### Customizations

Expand All @@ -62,6 +67,7 @@ You can use AuthFoundation when you want to:
- ``JWKValidator``
- ``TokenHashValidator``
- ``IDTokenValidator``
- ``IDTokenValidatorContext``

### Networking

Expand All @@ -74,6 +80,9 @@ You can use AuthFoundation when you want to:
- ``APIRequestArgument``
- ``APIRequestMethod``
- ``APIResponse``
- ``APIResponseResult``
- ``APIRateLimit``
- ``APIRetry``
- ``APIAuthorization``
- ``APIParsingContext``
- ``OAuth2APIRequest``
Expand All @@ -91,11 +100,13 @@ You can use AuthFoundation when you want to:
- ``JWTError``
- ``KeychainError``
- ``AuthenticationError``
- ``JSONValueError``

### Migration and versioning

- ``SDKVersion``
- ``SDKVersionMigrator``
- ``Version``

### Internals and mocking

Expand Down
76 changes: 76 additions & 0 deletions Sources/AuthFoundation/AuthFoundation.docc/WorkingWithClaims.md
Original file line number Diff line number Diff line change
@@ -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
}
```
79 changes: 79 additions & 0 deletions Sources/AuthFoundation/JWT/Enums/AuthenticationMethod.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// Copyright (c) 2024-Present, Okta, Inc. and/or its affiliates. All rights reserved.
// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
//
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and limitations under the License.
//

import Foundation

/// Defines the possible values for Authentication Methods, used within ``JWT/authenticationMethods``.
public enum AuthenticationMethod: String, ClaimConvertable, IsClaim {
/// Facial recognition
case facialRecognition = "face"

/// Fingerprint biometric
case fingerprintBiometric = "fpt"

/// Geolocation
case geolocation = "geo"

/// Proof-of-possession of a hardware-secured key
case proofOfPossessionHardware = "hwk"

/// Iris scan biometric
case irisScanBiometric = "iris"

/// Knowledge-based authentication
case knowledgeBased = "kba"

/// Multiple-channel authentication
case multipleChannel = "mca"

/// Multiple-factor authentication
case multipleFactor = "mfa"

/// One-time password
case oneTimePassword = "otp"

/// Personal Identification Number or pattern
case pin

/// Proof-of-possession of a key
case proofOfPossession = "pop"

/// Password-based authentication
case passwordBased = "pwd"

/// Risk-based authentication
case riskBased = "rba"

/// Retina scan biometric
case retinaScanBiometric = "retina"

/// Smart card
case smartCard = "sc"

/// Confirmation using SMS
case smsConfirmation = "sms"

/// Proof-of-possession of a software-secured key
case proofOfPossessionSoftware = "swk"

/// Confirmation by telephone call
case telephoneConfirmation = "tel"

/// User presence test
case userPresence = "user"

/// Voice biometric
case voiceBiometric = "vbm"

/// Windows integrated authentication
case windowsIntegrated = "wia"
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (c) 2021-Present, Okta, Inc. and/or its affiliates. All rights reserved.
// Copyright (c) 2024-Present, Okta, Inc. and/or its affiliates. All rights reserved.
// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
//
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
Expand All @@ -12,8 +12,11 @@

import Foundation

@available(*, deprecated, renamed: "JWTClaim")
public typealias Claim = JWTClaim

/// List of registered and public claims.
public enum Claim: Codable {
public enum JWTClaim: Codable, IsClaim {
/// Issuer
case issuer

Expand Down Expand Up @@ -229,62 +232,24 @@ public enum Claim: Codable {

/// Token introspection response
case tokenIntrospection

/// Custom claim with the given name
case custom(_ name: String)
}

/// Used by classes that contains OAuth2 claims.
///
/// This provides common conveniences for interacting with user or token information within those claims. For example, iterating through ``allClaims-4c54a`` or using keyed subscripting to access specific claims.
public protocol HasClaims {
/// Returns the collection of claims this object contains.
///
/// > Note: This will only return the list of official claims defined in the ``Claim`` enum. For custom claims, please see the ``customClaims`` property.
var claims: [Claim] { get }

/// Returns the collection of custom claims this object contains.
///
/// Unlike the ``claims`` property, this returns values as strings.
var customClaims: [String] { get }
/// 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

/// All claims, across both standard ``claims`` and ``customClaims``.
var allClaims: [String] { get }
/// Indicates the liklihood of whether or not this appears to be a real user. Used predominantly by Sign In With Apple.
case realUserStatus

/// Raw paylaod of claims, as a dictionary representation.
var payload: [String: Any] { get }
/// Indicates if the email address provided is a proxied address. Used predominantly by Sign In With Apple.
case isPrivateEmail

/// Return the given claim's value.
subscript<T>(_ claim: Claim) -> T? { get }
/// Identifier used when transfering subjects. Used predominantly by Sign In With Apple.
case transferSubject

/// Return the given claim's value.
subscript<T>(_ claim: String) -> T? { get }

/// Return the value of the requested claim, for the given type.
func value<T>(_ type: T.Type, for key: String) -> T?
/// Custom claim with the given name
case custom(_ name: String)
}

public extension HasClaims {
subscript<T>(_ claim: Claim) -> T? {
self[claim.rawValue]
}

subscript<T>(_ claim: String) -> T? {
if T.self == Date.self {
guard let time = value(Int.self, for: claim) else { return nil }
return Date(timeIntervalSince1970: TimeInterval(time)) as? T
} else {
return value(T.self, for: claim)
}
}

var allClaims: [String] {
Array([
claims.map(\.rawValue),
customClaims
].joined())
}

public extension HasClaims where ClaimType == JWTClaim {
/// The subject of the resource, if available.
var subject: String? { self[.subject] }

Expand Down Expand Up @@ -392,12 +357,12 @@ public extension HasClaims {
/// Returns the Authentication Context Class Reference for this token.
var authenticationClass: String? { self[.authContextClassReference] }

/// The list of authentication methods included in this token.
/// The list of authentication methods included in this token, which defines the list of methods that were used to authenticate the user.
///
/// ```swift
/// if jwt.authenticationMethods?.contains("mfa") {
/// if jwt.authenticationMethods?.contains(.multiFactor) {
/// // The user authenticated with an MFA factor.
/// }
/// ```
var authenticationMethods: [String]? { self[.authMethodsReference] }
var authenticationMethods: [AuthenticationMethod]? { arrayValue(AuthenticationMethod.self, for: .authMethodsReference) }
}
18 changes: 17 additions & 1 deletion Sources/AuthFoundation/JWT/Extensions/Claim+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Foundation

// swiftlint:disable function_body_length
// swiftlint:disable cyclomatic_complexity
extension Claim: RawRepresentable, Equatable {
extension JWTClaim: RawRepresentable, Equatable {
public typealias RawValue = String

public init?(rawValue: String) {
Expand Down Expand Up @@ -165,6 +165,14 @@ extension Claim: 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)
}
Expand Down Expand Up @@ -318,6 +326,14 @@ extension Claim: 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
}
Expand Down
Loading

0 comments on commit 99d57ce

Please sign in to comment.