Skip to content

Commit b00ca94

Browse files
feat(auth): Add OIDC support types & docs (#3064) (#3082)
* feat(auth): Add definitions for supporting OIDC plugin * chore: OIDC support API docs * feat: Add default reset method for Plugins * test: Add TaskQueue sync test; chore: Document TaskQueue * feat: Add default conformance to AuthAWSCredentialsProvider.getAWSCredentials() * feat: Add default conformance for AuthCognitoIdentityProvider methods --------- Co-authored-by: Tim Schmelter <[email protected]>
1 parent 4c7a84b commit b00ca94

File tree

7 files changed

+181
-5
lines changed

7 files changed

+181
-5
lines changed

Amplify/Categories/Auth/AuthCategoryBehavior.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public protocol AuthCategoryBehavior: AuthCategoryUserBehavior, AuthCategoryDevi
4949
/// SignIn to the authentication provider
5050
///
5151
/// Username and password are optional values, check the plugin documentation to decide on what all values need to
52-
/// passed. For example in a passwordless flow you just need to pass the username and the passwordcould be nil.
52+
/// passed. For example in a passwordless flow you just need to pass the username and the password could be nil.
5353
///
5454
/// - Parameters:
5555
/// - username: Username to signIn the user

Amplify/Categories/Auth/Models/AuthSession.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,19 @@ import Foundation
1010
/// Defines the auth session behavior
1111
public protocol AuthSession {
1212

13-
/// Indicates whether a user is signed in or not
13+
/// True if the current user has signed in
1414
///
15-
/// `true` if a user is authenticated. `isSignedIn` remains `true` till we call `Amplify.Auth.signOut`.
16-
/// Please note that this value remains `true` even when the session is expired. Refer the underlying plugin
17-
/// documentation regarding how to handle session expiry.
15+
/// App developers can use this flag to make local decisions about the content to display (e.g., "Should I show this protected
16+
/// page?") but cannot use it to make any further assertions about whether a network operation is likely to succeed or not.
17+
///
18+
/// `true` if a user has authenticated, via any of:
19+
/// - ``AuthCategoryBehavior/signIn(username:password:options:)``
20+
/// - ``AuthCategoryBehavior/signInWithWebUI(presentationAnchor:options:)``
21+
/// - ``AuthCategoryBehavior/signInWithWebUI(for:presentationAnchor:options:)``
22+
/// - A plugin-specific sign in method like
23+
/// `AWSCognitoAuthPlugin.federateToIdentityPool(withProviderToken:for:options:)`
24+
///
25+
/// `isSignedIn` remains `true` until we call `Amplify.Auth.signOut`. Notably, this value remains `true`
26+
/// even when the session is expired. Refer the underlying plugin documentation regarding how to handle session expiry.
1827
var isSignedIn: Bool { get }
1928
}

Amplify/Core/Support/TaskQueue.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,23 @@
77

88
import Foundation
99

10+
/// A helper for executing asynchronous work serially.
1011
public actor TaskQueue<Success> {
1112
private var previousTask: Task<Success, Error>?
1213

1314
public init() {}
1415

16+
/// Serializes asynchronous requests made from an async context
17+
///
18+
/// Given an invocation like
19+
/// ```swift
20+
/// let tq = TaskQueue<Int>()
21+
/// let v1 = try await tq.sync { try await doAsync1() }
22+
/// let v2 = try await tq.sync { try await doAsync2() }
23+
/// let v3 = try await tq.sync { try await doAsync3() }
24+
/// ```
25+
/// TaskQueue serializes this work so that `doAsync1` is performed before `doAsync2`,
26+
/// which is performed before `doAsync3`.
1527
public func sync(block: @Sendable @escaping () async throws -> Success) async throws -> Success {
1628
let currentTask: Task<Success, Error> = Task { [previousTask] in
1729
_ = await previousTask?.result
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import Amplify
10+
11+
/// Defines the contract for an AuthSession that can vend AWS credentials and store a user ID
12+
/// (`sub`) for the underlying OIDC-compliant authentication provider such as Cognito user pools.
13+
/// Concrete types that use Cognito identity pools to obtain AWS credentials can also vend the
14+
/// associated identityID.
15+
///
16+
/// **The `isSignedIn` property**
17+
///
18+
/// Types conforming to the `AuthSession` protocol support an `isSignedIn` flag. `isSignedIn` is true if a user has
19+
/// successfully completed a `signIn` flow, and has not subsequently signed out.
20+
public protocol AWSAuthSessionBehavior<Tokens> : AuthSession {
21+
22+
/// The concrete type holding the OIDC tokens from the authentication provider. Generally, this type will have at least
23+
/// methods for retrieving an identity token and an access token.
24+
associatedtype Tokens
25+
26+
/// The result of the most recent attempt to get AWS Credentials. There is no guarantee that the credentials
27+
/// are not expired, but conforming types may have logic in place to automatically refresh the credentials.
28+
/// The credentials may be fore either the unauthenticated or authenticated role, depending on the configuration of the
29+
/// identity pool and the tokens used to retrieve the identity ID from Cognito.
30+
///
31+
/// If the most recent attempt caused an error, the result will contain the details of the error.
32+
var awsCredentialsResult: Result<AWSTemporaryCredentials, AuthError> { get }
33+
34+
/// The result of the most recent attempt to get a
35+
/// [Cognito identity pool identity ID](https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_GetId.html#CognitoIdentity-GetId-response-IdentityId).
36+
/// The identityID may represent either an unauthenticated or authenticated identity, depending on the configuration of the
37+
/// identity pool and the tokens used to retrieve the identity ID from Cognito.
38+
///
39+
/// If the most recent attempt caused an error, the result will contain the details of the error.
40+
var identityIdResult: Result<String, AuthError> { get }
41+
42+
/// The result of the most recent attempt to get the current user's `sub` (unique User ID). Depending on the underlying
43+
/// implementation, the details of the user ID may vary, but it is expected that this value is the `sub` claim of the
44+
/// OIDC identity and access tokens.
45+
///
46+
/// If the most recent attempt caused an error, the result will contain the details of the error.
47+
var userSubResult: Result<String, AuthError> { get }
48+
49+
/// The result of the most recent attempt to get the current user's `sub` (unique User ID). Depending on the underlying
50+
/// implementation, the details of the tokens may vary, but it is expected that the type will have at least methods for
51+
/// retrieving an identity token and an access token.
52+
///
53+
/// If the most recent attempt caused an error, the result will contain the details of the error.
54+
var oidcTokensResult: Result<Tokens, AuthError> { get }
55+
}

AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthAWSCredentialsProvider.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,28 @@ import Amplify
99
import Foundation
1010

1111
public protocol AuthAWSCredentialsProvider {
12+
/// Return the most recent Result of fetching the AWS Credentials
1213
func getAWSCredentials() -> Result<AWSCredentials, AuthError>
1314
}
1415

16+
public extension AuthAWSCredentialsProvider where Self:AWSAuthSessionBehavior {
17+
/// Return the most recent Result of fetching the AWS Credentials. If the temporary credentials are expired, returns
18+
/// a `AuthError.sessionExpired` failure.
19+
func getAWSCredentials() -> Result<AWSCredentials, AuthError> {
20+
let result: Result<AWSCredentials, AuthError>
21+
switch awsCredentialsResult {
22+
case .failure(let error): result = .failure(error)
23+
case .success(let tempCreds):
24+
if tempCreds.expiration > Date() {
25+
result = .success(tempCreds)
26+
} else {
27+
result = .failure(AuthError.sessionExpired("AWS Credentials are expired", ""))
28+
}
29+
}
30+
return result
31+
}
32+
}
33+
1534
public protocol AWSCredentialsProvider {
1635
func fetchAWSCredentials() async throws -> AWSCredentials
1736
}

AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthCognitoIdentityProvider.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,19 @@
88
import Amplify
99

1010
public protocol AuthCognitoIdentityProvider {
11+
/// Return the most recent Result of fetching the AWS Cognito Identity Pools identity ID
1112
func getIdentityId() -> Result<String, AuthError>
1213

14+
/// Return the most recent Result of the current user’s sub claim (user ID)
1315
func getUserSub() -> Result<String, AuthError>
1416
}
17+
18+
public extension AuthCognitoIdentityProvider where Self:AWSAuthSessionBehavior {
19+
func getIdentityId() -> Result<String, AuthError> {
20+
identityIdResult
21+
}
22+
23+
func getUserSub() -> Result<String, AuthError> {
24+
userSubResult
25+
}
26+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//
2+
// Copyright Amazon.com Inc. or its affiliates.
3+
// All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import XCTest
9+
import Amplify
10+
11+
// As expected, running this work as straight up Task invocations:
12+
13+
// ```
14+
// try await Task {
15+
// try await Task.sleep(nanoseconds: 1)
16+
// expectation1.fulfill()
17+
// }
18+
// try await Task {
19+
// try await Task.sleep(nanoseconds: 1)
20+
// expectation2.fulfill()
21+
// }
22+
// try await Task {
23+
// try await Task.sleep(nanoseconds: 1)
24+
// expectation3.fulfill()
25+
// }
26+
// await fulfillment(of: [expectation1, expectation2, expectation3], enforceOrder: true)
27+
// ```
28+
//
29+
// does not guarantee order of execution. TaskQueue is intended to serialize execution to guarantee
30+
// that the in-process task completes before the new one is executed.
31+
32+
final class AmplifyTaskQueueTests: XCTestCase {
33+
34+
/// Test basic TaskQueue.sync behavior
35+
///
36+
/// - Given: A task queue
37+
/// - When: I add tasks to the queue using the `sync` method
38+
/// - Then: The tasks execute in the order added
39+
func testSync() async throws {
40+
for _ in 1 ... 1_000 {
41+
try await doSyncTest()
42+
}
43+
}
44+
45+
func doSyncTest() async throws {
46+
let expectation1 = expectation(description: "expectation1")
47+
let expectation2 = expectation(description: "expectation2")
48+
let expectation3 = expectation(description: "expectation3")
49+
50+
let taskQueue = TaskQueue<Void>()
51+
try await taskQueue.sync {
52+
try await Task.sleep(nanoseconds: 1)
53+
expectation1.fulfill()
54+
}
55+
56+
try await taskQueue.sync {
57+
try await Task.sleep(nanoseconds: 1)
58+
expectation2.fulfill()
59+
}
60+
61+
try await taskQueue.sync {
62+
try await Task.sleep(nanoseconds: 1)
63+
expectation3.fulfill()
64+
}
65+
66+
await fulfillment(of: [expectation1, expectation2, expectation3], enforceOrder: true)
67+
}
68+
69+
}

0 commit comments

Comments
 (0)