Skip to content

Commit

Permalink
feat(auth): adding passwordless sign in preferred flows (#162)
Browse files Browse the repository at this point in the history
* feat(auth): add passwordless preferred flow

* adding confirm device and device srp flows to user auth

* update message

* worked on review comments

* update
  • Loading branch information
harsh62 authored Nov 4, 2024
1 parent c773ced commit 56b31e0
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,29 +37,46 @@ struct InitiateUserAuth: Action {
return
}

let preferredChallenge: String?
let preferredChallengeAuthParams: [String: String]
let srpStateData: SRPStateData?
if case .apiBased(let authFlow) = signInEventData.signInMethod,
case .userAuth(let firstFactor) = authFlow {
preferredChallenge = firstFactor?.challengeResponse
case .userAuth(let firstFactor) = authFlow,
let authFactor = firstFactor {
let preferredChallengeHelper = PreferredChallengeHelper(
authFactor: authFactor,
password: signInEventData.password,
username: username,
environment: environment)
preferredChallengeAuthParams = try preferredChallengeHelper.toCognitoAuthParameters()
srpStateData = preferredChallengeHelper.srpStateData
} else {
preferredChallenge = nil
preferredChallengeAuthParams = [:]
srpStateData = nil
}


let asfDeviceId = try await CognitoUserPoolASF.asfDeviceID(
for: username,
credentialStoreClient: authEnv.credentialsClient)
let request = await InitiateAuthInput.userAuth(
username: username,
preferredChallenge: preferredChallenge,
preferredChallengeAuthParams: preferredChallengeAuthParams,
clientMetadata: signInEventData.clientMetadata,
asfDeviceId: asfDeviceId,
deviceMetadata: deviceMetadata,
environment: userPoolEnv)

let responseEvent = try await sendRequest(
request: request,
username: username,
environment: userPoolEnv)
let cognitoClient = try userPoolEnv.cognitoUserPoolFactory()
logVerbose("\(#fileID) Starting execution", environment: environment)
let response = try await cognitoClient.initiateAuth(input: request)
let responseEvent = UserPoolSignInHelper.parseResponse(
response,
for: username,
signInMethod: signInEventData.signInMethod,
presentationAnchor: signInEventData.presentationAnchor,
srpStateData: srpStateData
)

logVerbose("\(#fileID) Sending event \(responseEvent)", environment: environment)
await dispatcher.send(responseEvent)

Expand All @@ -76,22 +93,6 @@ struct InitiateUserAuth: Action {
await dispatcher.send(event)
}
}

private func sendRequest(request: InitiateAuthInput,
username: String,
environment: UserPoolEnvironment) async throws -> StateMachineEvent {

let cognitoClient = try environment.cognitoUserPoolFactory()
logVerbose("\(#fileID) Starting execution", environment: environment)

let response = try await cognitoClient.initiateAuth(input: request)
return UserPoolSignInHelper.parseResponse(
response,
for: username,
signInMethod: signInEventData.signInMethod,
presentationAnchor: signInEventData.presentationAnchor
)
}
}

extension InitiateUserAuth: DefaultLogger { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,6 @@ extension MigrateSignInState {
case .error: return "MigrateSignInState.error"
}
}

static func == (lhs: MigrateSignInState, rhs: MigrateSignInState) -> Bool {
switch (lhs, rhs) {
case (.notStarted, .notStarted):
return true
case (.signingIn(let lhsData), .signingIn(let rhsData)):
return lhsData == rhsData
case (.signedIn(let lhsData), .signedIn(let rhsData)):
return lhsData == rhsData
case (.error(let lhsData), .error(let rhsData)):
return lhsData == rhsData
default:
return false
}
}
}

extension MigrateSignInState: Equatable { }
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,22 @@ extension SignInState {
actions: [SignInComplete(signedInData: signedInData)])
}

if let signInEvent = event as? SignInEvent,
case .confirmDevice(let signedInData) = signInEvent.eventType {
let action = ConfirmDevice(signedInData: signedInData)
return .init(newState: .confirmingDevice,
actions: [action])
}

if let signInEvent = event as? SignInEvent,
case .initiateDeviceSRP(let username, let challengeResponse) = signInEvent.eventType {
let action = StartDeviceSRPFlow(
username: username,
authResponse: challengeResponse)
return .init(newState: .resolvingDeviceSrpa(.notStarted),
actions: [action])
}

if let signInEvent = event as? SignInEvent,
case .receivedChallenge(let challenge) = signInEvent.eventType {
let action = InitializeResolveChallenge(challenge: challenge,
Expand All @@ -462,6 +478,18 @@ extension SignInState {
)
}

if case .respondPasswordVerifier(let srpStateData, let authResponse, let clientMetadata) = event.isSignInEvent {
let action = VerifyPasswordSRP(
stateData: srpStateData,
authResponse: authResponse,
clientMetadata: clientMetadata)
return .init(
newState: .signingInWithSRP(
.respondingPasswordVerifier(srpStateData),
signInEventData),
actions: [action])
}

if let signInEvent = event as? SignInEvent,
case .throwAuthError(let error) = signInEvent.eventType {
let action = ThrowSignInError(error: error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,15 @@ extension AuthPluginErrorConstants {
"""
)

static let confirmSignInFactorSelectionResponseError: AuthPluginValidationErrorString = (
"challengeResponse",
"challengeResponse for factor selection can only be one of the `AuthFactorType` values.",
"""
Make sure that a valid challenge response is passed for confirmSignIn.
Try using `AuthFactorType.<type>.challengeResponse` as the challenge response.
"""
)

static let confirmResetPasswordUsernameError: AuthPluginValidationErrorString = (
"username",
"username is required to confirmResetPassword",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//

import Amplify
import Foundation

class PreferredChallengeHelper {

let authFactor: AuthFactorType
let password: String?
let username: String
let environment: Environment
private(set) var srpStateData: SRPStateData? = nil

init(authFactor: AuthFactorType,
password: String?,
username: String,
environment: Environment) {
self.authFactor = authFactor
self.password = password
self.username = username
self.environment = environment
}

func toCognitoAuthParameters() throws -> [String: String] {
var authParameters: [String: String] = [:]
authParameters["PREFERRED_CHALLENGE"] = authFactor.challengeResponse

switch authFactor {
case .password:
guard let password = password else {
throw AuthError.validation(
AuthPluginErrorConstants.signInPasswordError.field,
AuthPluginErrorConstants.signInPasswordError.errorDescription,
AuthPluginErrorConstants.signInPasswordError.recoverySuggestion)
}
authParameters["PASSWORD"] = password
case .passwordSRP:
let srpStateData = try generateSRPStateData()
authParameters["SRP_A"] = srpStateData.srpKeyPair.publicKeyHexValue
default:
break
}
return authParameters
}

private func generateSRPStateData() throws -> SRPStateData {
let srpEnv = try environment.srpEnvironment()
let nHexValue = srpEnv.srpConfiguration.nHexValue
let gHexValue = srpEnv.srpConfiguration.gHexValue

let srpClient = try SRPSignInHelper.srpClient(srpEnv)
let srpKeyPair = srpClient.generateClientKeyPair()
guard let password = password else {
throw AuthError.validation(
AuthPluginErrorConstants.signInPasswordError.field,
AuthPluginErrorConstants.signInPasswordError.errorDescription,
AuthPluginErrorConstants.signInPasswordError.recoverySuggestion)
}
let srpStateData = SRPStateData(
username: username,
password: password,
NHexValue: nHexValue,
gHexValue: gHexValue,
srpKeyPair: srpKeyPair,
clientTimestamp: Date())
self.srpStateData = srpStateData
return srpStateData
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@ struct UserPoolSignInHelper: DefaultLogger {
_ response: SignInResponseBehavior,
for username: String,
signInMethod: SignInMethod,
presentationAnchor: AuthUIPresentationAnchor? = nil
presentationAnchor: AuthUIPresentationAnchor? = nil,
srpStateData: SRPStateData? = nil
) -> StateMachineEvent {

if let authenticationResult = response.authenticationResult,
Expand Down Expand Up @@ -122,6 +123,15 @@ struct UserPoolSignInHelper: DefaultLogger {
switch challengeName {
case .smsMfa, .customChallenge, .newPasswordRequired, .softwareTokenMfa, .selectMfaType, .smsOtp, .emailOtp, .selectChallenge:
return SignInEvent(eventType: .receivedChallenge(respondToAuthChallenge))
case .passwordVerifier:
guard let srpStateData else {
let message = "Unable to extract SRP state data to continue with password verification."
let error = SignInError.invalidServiceResponse(message: message)
return SignInEvent(eventType: .throwAuthError(error))
}
return SignInEvent(
eventType: .respondPasswordVerifier(srpStateData, response, [:])
)
case .deviceSrpAuth:
return SignInEvent(eventType: .initiateDeviceSRP(username, response))
case .webAuthn:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,13 @@ extension InitiateAuthInput {
}

static func userAuth(username: String,
preferredChallenge: String?,
preferredChallengeAuthParams: [String: String],
clientMetadata: [String: String],
asfDeviceId: String,
deviceMetadata: DeviceMetadata,
environment: UserPoolEnvironment) async -> InitiateAuthInput {
var authParameters = [
"USERNAME": username
]
if let preferredChallenge {
authParameters["PREFERRED_CHALLENGE"] = preferredChallenge
}
var authParameters = preferredChallengeAuthParams
authParameters["USERNAME"] = username

return await buildInput(username: username,
authFlowType: .userAuth,
Expand Down
Loading

0 comments on commit 56b31e0

Please sign in to comment.