Skip to content

Commit 7eaa499

Browse files
committed
feat(auth): add support for cognito oidc parameters in managed login
1 parent 0340886 commit 7eaa499

File tree

7 files changed

+170
-5
lines changed

7 files changed

+170
-5
lines changed

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Actions/CredentialStore/MigrateLegacyCredentialStore.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,12 @@ struct MigrateLegacyCredentialStore: Action {
316316
scopes: scopes ?? [],
317317
providerInfo: provider,
318318
presentationAnchor: nil,
319-
preferPrivateSession: false
319+
preferPrivateSession: false,
320+
nonce: nil,
321+
language: nil,
322+
loginHint: nil,
323+
prompt: nil,
324+
resource: nil
320325
))
321326
default:
322327
return .apiBased(.userSRP)

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Models/Options/AWSAuthWebUISignInOptions.swift

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,76 @@ public struct AWSAuthWebUISignInOptions {
2727
/// Safari always honors the request.
2828
public let preferPrivateSession: Bool
2929

30+
/// A random value that you can add to the request. The nonce value that you provide is included in the ID token
31+
/// that Amazon Cognito issues. To guard against replay attacks, your app can inspect the nonce claim in the ID
32+
/// token and compare it to the one you generated.
33+
public let nonce: String?
34+
35+
/// The language that you want to display user-interactive pages in
36+
/// For more information, see Managed login localization -
37+
/// https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-managed-login.html#managed-login-localization
38+
public let language: String?
39+
40+
/// A username prompt that you want to pass to the authorization server. You can collect a username, email
41+
/// address or phone number from your user and allow the destination provider to pre-populate the user's
42+
/// sign-in name.
43+
public let loginHint: String?
44+
45+
/// An OIDC parameter that controls authentication behavior for existing sessions.
46+
public let prompt: [Prompt]?
47+
48+
/// The identifier of a resource that you want to bind to the access token in the `aud` claim. When you include
49+
/// this parameter, Amazon Cognito validates that the value is a URL and sets the audience of the resulting
50+
/// access token to the requested resource. Values for this parameter must begin with "https://", "http://localhost",
51+
/// or a custom URL scheme like "myapp://".
52+
public let resource: String?
53+
3054
public init(
3155
idpIdentifier: String? = nil,
32-
preferPrivateSession: Bool = false
56+
preferPrivateSession: Bool = false,
57+
nonce: String? = nil,
58+
language: String? = nil,
59+
loginHint: String? = nil,
60+
prompt: [Prompt]? = nil,
61+
resource: String? = nil
3362
) {
3463
self.idpIdentifier = idpIdentifier
3564
self.preferPrivateSession = preferPrivateSession
65+
self.nonce = nonce
66+
self.language = language
67+
self.loginHint = loginHint
68+
self.prompt = prompt
69+
self.resource = resource
70+
}
71+
}
72+
73+
public extension AWSAuthWebUISignInOptions {
74+
75+
enum Prompt: String, Codable {
76+
/// Amazon Cognito silently continues authentication for users who have a valid authenticated session.
77+
/// With this prompt, users can silently authenticate between different app clients in your user pool.
78+
/// If the user is not already authenticated, the authorization server returns a login_required error.
79+
case none
80+
81+
/// Amazon Cognito requires users to re-authenticate even if they have an existing session. Send this
82+
/// value when you want to verify the user's identity again. Authenticated users who have an existing
83+
/// session can return to sign-in without invalidating that session. When a user who has an existing
84+
/// session signs in again, Amazon Cognito assigns them a new session cookie. This parameter can also
85+
/// be forwarded to your IdPs. IdPs that accept this parameter also request a new authentication
86+
/// attempt from the user.
87+
case login
88+
89+
/// This value has no effect on local sign-in and must be submitted in requests that redirect to IdPs.
90+
/// When included in your authorization request, this parameter adds prompt=select_account to the URL
91+
/// path for the IdP redirect destination. When IdPs support this parameter, they request that users
92+
/// select the account that they want to log in with.
93+
case selectAccount = "select_account"
94+
95+
/// This value has no effect on local sign-in and must be submitted in requests that redirect to IdPs.
96+
/// When included in your authorization request, this parameter adds prompt=consent to the URL path for
97+
/// the IdP redirect destination. When IdPs support this parameter, they request user consent before
98+
/// they redirect back to your user pool.
99+
case consent
36100
}
37101
}
38102

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Operations/Helpers/HostedUISignInHelper.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,12 @@ struct HostedUISignInHelper: DefaultLogger {
131131
scopes: request.options.scopes ?? scopeFromConfig,
132132
providerInfo: providerInfo,
133133
presentationAnchor: request.presentationAnchor,
134-
preferPrivateSession: privateSession
134+
preferPrivateSession: privateSession,
135+
nonce: pluginOptions?.nonce,
136+
language: pluginOptions?.language,
137+
loginHint: pluginOptions?.loginHint,
138+
prompt: pluginOptions?.prompt,
139+
resource: pluginOptions?.resource
135140
)
136141
let signInData = SignInEventData(
137142
username: nil,

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/StateMachine/CodeGen/Data/HostedUIOptions.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ struct HostedUIOptions {
1717
let presentationAnchor: AuthUIPresentationAnchor?
1818

1919
let preferPrivateSession: Bool
20+
21+
let nonce: String?
22+
23+
let language: String?
24+
25+
let loginHint: String?
26+
27+
let prompt: [AWSAuthWebUISignInOptions.Prompt]?
28+
29+
let resource: String?
2030
}
2131

2232
extension HostedUIOptions: Codable {
@@ -28,6 +38,16 @@ extension HostedUIOptions: Codable {
2838
case providerInfo
2939

3040
case preferPrivateSession
41+
42+
case nonce
43+
44+
case language = "lang"
45+
46+
case loginHint = "login_hint"
47+
48+
case prompt
49+
50+
case resource
3151
}
3252

3353
init(from decoder: Decoder) throws {
@@ -36,13 +56,23 @@ extension HostedUIOptions: Codable {
3656
self.providerInfo = try values.decode(HostedUIProviderInfo.self, forKey: .providerInfo)
3757
self.preferPrivateSession = try values.decode(Bool.self, forKey: .preferPrivateSession)
3858
self.presentationAnchor = nil
59+
self.nonce = try values.decode(String.self, forKey: .nonce)
60+
self.language = try values.decode(String.self, forKey: .language)
61+
self.loginHint = try values.decode(String.self, forKey: .loginHint)
62+
self.prompt = try values.decode(Array.self, forKey: .prompt)
63+
self.resource = try values.decode(String.self, forKey: .resource)
3964
}
4065

4166
func encode(to encoder: Encoder) throws {
4267
var container = encoder.container(keyedBy: CodingKeys.self)
4368
try container.encode(scopes, forKey: .scopes)
4469
try container.encode(providerInfo, forKey: .providerInfo)
4570
try container.encode(preferPrivateSession, forKey: .preferPrivateSession)
71+
try container.encode(nonce, forKey: .nonce)
72+
try container.encode(language, forKey: .language)
73+
try container.encode(loginHint, forKey: .loginHint)
74+
try container.encodeIfPresent(prompt, forKey: .prompt)
75+
try container.encode(resource, forKey: .resource)
4676
}
4777
}
4878

AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/HostedUI/HostedUIRequestHelper.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,22 @@ enum HostedUIRequestHelper {
5858
components.queryItems?.append(
5959
.init(name: "identity_provider", value: authProvider.userPoolProviderName))
6060
}
61+
if let nonce = options.nonce {
62+
components.queryItems?.append(.init(name: "nonce", value: nonce))
63+
}
64+
if let language = options.language {
65+
components.queryItems?.append(.init(name: "lang", value: language))
66+
}
67+
if let loginHint = options.loginHint {
68+
components.queryItems?.append(.init(name: "login_hint", value: loginHint))
69+
}
70+
if let prompt = options.prompt {
71+
let promptValue = prompt.map{ "\($0.rawValue)" }.joined(separator: " ")
72+
components.queryItems?.append(.init(name: "prompt", value: promptValue))
73+
}
74+
if let resource = options.resource {
75+
components.queryItems?.append(.init(name: "resource", value: resource))
76+
}
6177

6278
guard let url = components.url else {
6379
throw HostedUIError.signInURI

AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Mocks/MockData/SignedInData+Mock.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@ extension SignedInData {
3636
scopes: [],
3737
providerInfo: .init(authProvider: .google, idpIdentifier: ""),
3838
presentationAnchor: nil,
39-
preferPrivateSession: false
39+
preferPrivateSession: false,
40+
nonce: nil,
41+
language: nil,
42+
loginHint: nil,
43+
prompt: nil,
44+
resource: nil
4045
)),
4146
cognitoUserPoolTokens: tokens
4247
)

AmplifyPlugins/Auth/Tests/AWSCognitoAuthPluginUnitTests/Support/HostedUIRequestHelperTests.swift

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ class HostedUIRequestHelperTests: XCTestCase {
2424
idpIdentifier: nil
2525
),
2626
presentationAnchor: nil,
27-
preferPrivateSession: false
27+
preferPrivateSession: false,
28+
nonce: nil,
29+
language: nil,
30+
loginHint: nil,
31+
prompt: nil,
32+
resource: nil
2833
)
2934
)
3035

@@ -83,4 +88,39 @@ class HostedUIRequestHelperTests: XCTestCase {
8388
let encodedSecret = try XCTUnwrap(encodedSecret)
8489
XCTAssertEqual("Basic \(encodedSecret)", header)
8590
}
91+
92+
/// Given: A HostedUI configuration that defines a client secret
93+
/// When: HostedUIRequestHelper.createSignInURL is invoked with cognito oidc parameters
94+
/// Then: A URL is generated that contains all the cognito oidc parameters in url query parameters
95+
func testCreateSignInURL_withCognitoOIDCParametersInOptions_shouldContainOIDCParametersInURLQueryParams() throws {
96+
createConfiguration(clientSecret: "clientSecret")
97+
let signInURL = try HostedUIRequestHelper.createSignInURL(
98+
state: "state",
99+
proofKey: "proofKey",
100+
userContextData: nil,
101+
configuration: configuration,
102+
options: .init(
103+
scopes: [],
104+
providerInfo: .init(authProvider: nil, idpIdentifier: nil),
105+
presentationAnchor: nil,
106+
preferPrivateSession: false,
107+
nonce: "nonce",
108+
language: "en",
109+
loginHint: "username",
110+
prompt: [.login, .consent],
111+
resource: "http://localhost"
112+
)
113+
)
114+
115+
guard let urlComponents = URLComponents(url: signInURL, resolvingAgainstBaseURL: false) else {
116+
XCTFail("Failed to get URL components from \(signInURL)")
117+
return
118+
}
119+
120+
XCTAssertEqual("nonce", urlComponents.queryItems?.first(where: { $0.name == "nonce"})?.value)
121+
XCTAssertEqual("en", urlComponents.queryItems?.first(where: { $0.name == "lang"})?.value)
122+
XCTAssertEqual("username", urlComponents.queryItems?.first(where: { $0.name == "login_hint"})?.value)
123+
XCTAssertEqual("login consent", urlComponents.queryItems?.first(where: { $0.name == "prompt"})?.value)
124+
XCTAssertEqual("http://localhost", urlComponents.queryItems?.first(where: { $0.name == "resource"})?.value)
125+
}
86126
}

0 commit comments

Comments
 (0)