Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,12 @@ struct MigrateLegacyCredentialStore: Action {
scopes: scopes ?? [],
providerInfo: provider,
presentationAnchor: nil,
preferPrivateSession: false
preferPrivateSession: false,
nonce: nil,
language: nil,
loginHint: nil,
prompt: nil,
resource: nil
))
default:
return .apiBased(.userSRP)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,76 @@ public struct AWSAuthWebUISignInOptions {
/// Safari always honors the request.
public let preferPrivateSession: Bool

/// A random value that you can add to the request. The nonce value that you provide is included in the ID token
/// that Amazon Cognito issues. To guard against replay attacks, your app can inspect the nonce claim in the ID
/// token and compare it to the one you generated.
public let nonce: String?

/// The language that you want to display user-interactive pages in
/// For more information, see Managed login localization -
/// https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-managed-login.html#managed-login-localization
public let language: String?

/// A username prompt that you want to pass to the authorization server. You can collect a username, email
/// address or phone number from your user and allow the destination provider to pre-populate the user's
/// sign-in name.
public let loginHint: String?

/// An OIDC parameter that controls authentication behavior for existing sessions.
public let prompt: [Prompt]?

/// The identifier of a resource that you want to bind to the access token in the `aud` claim. When you include
/// this parameter, Amazon Cognito validates that the value is a URL and sets the audience of the resulting
/// access token to the requested resource. Values for this parameter must begin with "https://", "http://localhost",
/// or a custom URL scheme like "myapp://".
public let resource: String?

public init(
idpIdentifier: String? = nil,
preferPrivateSession: Bool = false
preferPrivateSession: Bool = false,
nonce: String? = nil,
language: String? = nil,
loginHint: String? = nil,
prompt: [Prompt]? = nil,
resource: String? = nil
) {
self.idpIdentifier = idpIdentifier
self.preferPrivateSession = preferPrivateSession
self.nonce = nonce
self.language = language
self.loginHint = loginHint
self.prompt = prompt
self.resource = resource
}
}

public extension AWSAuthWebUISignInOptions {

enum Prompt: String, Codable {
/// Amazon Cognito silently continues authentication for users who have a valid authenticated session.
/// With this prompt, users can silently authenticate between different app clients in your user pool.
/// If the user is not already authenticated, the authorization server returns a login_required error.
case none

/// Amazon Cognito requires users to re-authenticate even if they have an existing session. Send this
/// value when you want to verify the user's identity again. Authenticated users who have an existing
/// session can return to sign-in without invalidating that session. When a user who has an existing
/// session signs in again, Amazon Cognito assigns them a new session cookie. This parameter can also
/// be forwarded to your IdPs. IdPs that accept this parameter also request a new authentication
/// attempt from the user.
case login

/// This value has no effect on local sign-in and must be submitted in requests that redirect to IdPs.
/// When included in your authorization request, this parameter adds prompt=select_account to the URL
/// path for the IdP redirect destination. When IdPs support this parameter, they request that users
/// select the account that they want to log in with.
case selectAccount = "select_account"

/// This value has no effect on local sign-in and must be submitted in requests that redirect to IdPs.
/// When included in your authorization request, this parameter adds prompt=consent to the URL path for
/// the IdP redirect destination. When IdPs support this parameter, they request user consent before
/// they redirect back to your user pool.
case consent
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,12 @@ struct HostedUISignInHelper: DefaultLogger {
scopes: request.options.scopes ?? scopeFromConfig,
providerInfo: providerInfo,
presentationAnchor: request.presentationAnchor,
preferPrivateSession: privateSession
preferPrivateSession: privateSession,
nonce: pluginOptions?.nonce,
language: pluginOptions?.language,
loginHint: pluginOptions?.loginHint,
promptValues: pluginOptions?.prompt,
resource: pluginOptions?.resource
)
let signInData = SignInEventData(
username: nil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ struct HostedUIOptions {
let presentationAnchor: AuthUIPresentationAnchor?

let preferPrivateSession: Bool

let nonce: String?

let language: String?

let loginHint: String?

let prompt: String?

let resource: String?
}

extension HostedUIOptions: Codable {
Expand All @@ -28,6 +38,16 @@ extension HostedUIOptions: Codable {
case providerInfo

case preferPrivateSession

case nonce

case language = "lang"

case loginHint = "login_hint"

case prompt

case resource
}

init(from decoder: Decoder) throws {
Expand All @@ -36,14 +56,52 @@ extension HostedUIOptions: Codable {
self.providerInfo = try values.decode(HostedUIProviderInfo.self, forKey: .providerInfo)
self.preferPrivateSession = try values.decode(Bool.self, forKey: .preferPrivateSession)
self.presentationAnchor = nil
self.nonce = try values.decode(String.self, forKey: .nonce)
self.language = try values.decode(String.self, forKey: .language)
self.loginHint = try values.decode(String.self, forKey: .loginHint)
self.prompt = try values.decode(String.self, forKey: .prompt)
self.resource = try values.decode(String.self, forKey: .resource)
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(scopes, forKey: .scopes)
try container.encode(providerInfo, forKey: .providerInfo)
try container.encode(preferPrivateSession, forKey: .preferPrivateSession)
try container.encode(nonce, forKey: .nonce)
try container.encode(language, forKey: .language)
try container.encode(loginHint, forKey: .loginHint)
try container.encodeIfPresent(prompt, forKey: .prompt)
try container.encode(resource, forKey: .resource)
}
}

extension HostedUIOptions: Equatable { }

#if os(iOS) || os(macOS) || os(visionOS)
extension HostedUIOptions {
init(
scopes: [String],
providerInfo: HostedUIProviderInfo,
presentationAnchor: AuthUIPresentationAnchor?,
preferPrivateSession: Bool,
nonce: String?,
language: String?,
loginHint: String?,
promptValues: [AWSAuthWebUISignInOptions.Prompt]?,
resource: String?
) {
self.init(
scopes: scopes,
providerInfo: providerInfo,
presentationAnchor: presentationAnchor,
preferPrivateSession: preferPrivateSession,
nonce: nonce,
language: language,
loginHint: loginHint,
prompt: promptValues?.map { "\($0.rawValue)" }.joined(separator: " "),
resource: resource
)
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ enum HostedUIRequestHelper {
components.queryItems?.append(
.init(name: "identity_provider", value: authProvider.userPoolProviderName))
}
if let nonce = options.nonce {
components.queryItems?.append(.init(name: "nonce", value: nonce))
}
if let language = options.language {
components.queryItems?.append(.init(name: "lang", value: language))
}
if let loginHint = options.loginHint {
components.queryItems?.append(.init(name: "login_hint", value: loginHint))
}
if let prompt = options.prompt {
components.queryItems?.append(.init(name: "prompt", value: prompt))
}
if let resource = options.resource {
components.queryItems?.append(.init(name: "resource", value: resource))
}

guard let url = components.url else {
throw HostedUIError.signInURI
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ extension SignedInData {
scopes: [],
providerInfo: .init(authProvider: .google, idpIdentifier: ""),
presentationAnchor: nil,
preferPrivateSession: false
preferPrivateSession: false,
nonce: nil,
language: nil,
loginHint: nil,
prompt: nil,
resource: nil
)),
cognitoUserPoolTokens: tokens
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ class HostedUIRequestHelperTests: XCTestCase {
idpIdentifier: nil
),
presentationAnchor: nil,
preferPrivateSession: false
preferPrivateSession: false,
nonce: nil,
language: nil,
loginHint: nil,
prompt: nil,
resource: nil
)
)

Expand Down Expand Up @@ -83,4 +88,41 @@ class HostedUIRequestHelperTests: XCTestCase {
let encodedSecret = try XCTUnwrap(encodedSecret)
XCTAssertEqual("Basic \(encodedSecret)", header)
}

#if os(iOS) || os(macOS) || os(visionOS)
/// Given: A HostedUI configuration that defines a client secret
/// When: HostedUIRequestHelper.createSignInURL is invoked with cognito oidc parameters
/// Then: A URL is generated that contains all the cognito oidc parameters in url query parameters
func testCreateSignInURL_withCognitoOIDCParametersInOptions_shouldContainOIDCParametersInURLQueryParams() throws {
createConfiguration(clientSecret: "clientSecret")
let signInURL = try HostedUIRequestHelper.createSignInURL(
state: "state",
proofKey: "proofKey",
userContextData: nil,
configuration: configuration,
options: .init(
scopes: [],
providerInfo: .init(authProvider: nil, idpIdentifier: nil),
presentationAnchor: nil,
preferPrivateSession: false,
nonce: "nonce",
language: "en",
loginHint: "username",
promptValues: [.login, .consent],
resource: "http://localhost"
)
)

guard let urlComponents = URLComponents(url: signInURL, resolvingAgainstBaseURL: false) else {
XCTFail("Failed to get URL components from \(signInURL)")
return
}

XCTAssertEqual("nonce", urlComponents.queryItems?.first(where: { $0.name == "nonce"})?.value)
XCTAssertEqual("en", urlComponents.queryItems?.first(where: { $0.name == "lang"})?.value)
XCTAssertEqual("username", urlComponents.queryItems?.first(where: { $0.name == "login_hint"})?.value)
XCTAssertEqual("login consent", urlComponents.queryItems?.first(where: { $0.name == "prompt"})?.value)
XCTAssertEqual("http://localhost", urlComponents.queryItems?.first(where: { $0.name == "resource"})?.value)
}
#endif
}
Loading