41
loading...
This website collects cookies to deliver better user experience
Please activate the experimental features in the Hanko Authentication API. To accomplish this, log into your Hanko Console, select the Relying Party, and navigate to the "General Settings". On the right side you will find the button to activate the experimental features.
Associated domains establish a secure association between domains and your app so you can share credentials or provide features in your app from your website.
webcredentials:yourdomain.com
).1ABC23DEF4
) and the generated Bundle Identifier (e.g. com.example.apple-samplecode.Shiny1ABC23DEF4
)config/config.yaml
file:
iosAppId: "<Team ID>.<Bundle Identifier>"
, e.g. iosAppId: "1ABC23DEF4.com.example.apple-samplecode.Shiny1ABC23DEF4"
let domain = "yourdomain.com"
AccountManager.swift
file - so let's start there. The original Shiny gives us some hints on where it needs to be amended.signInWith()
function. To sign a user in we first need to fetch a unique challenge from the Hanko Authentication API via our web app. This challenge will be signed with the passkey and returned to the web app for validation, again utilizing the Hanko Authentication API.signInWith()
function:let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: domain)
getAuthenticationOptions()
function and have the result available in the assertionRequestOptions
variable in the following function block:getAuthenticationOptions() { assertionRequestOptions in ... }
getAuthenticationOptions()
function itself basically calls our web app's /authentication_initialize
endpoint, we have used that endpoint in the browser flow as well:func getAuthenticationOptions(completionHandler: @escaping (CredentialAssertion) -> Void) {
AF.request("https://\(domain)/authentication_initialize", method: .get).responseDecodable(of: CredentialAssertion.self) { ... }
}
CredentialAssertion
object defined in the Models.swift
file. Go check it out - it is pretty straight forward. As Swift does only deal with plain Base64 we are using a helper function to convert the challenge:let challenge = assertionRequestOptions.publicKey.challenge.decodeBase64Url()!
decodeBase64Url()
function at the end of the AccountManager.swift
file. It takes an Base64URL string as an input. Basically it replaces a few characters and sorts out the padding. As a result it gives us plain Base64.publicKeyCredentialProvider
which we have created earlier for that:let assertionRequest = publicKeyCredentialProvider.createCredentialAssertionRequest(challenge: challenge)
if let userVerification = assertionRequestOptions.publicKey.userVerification {
assertionRequest.userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreference.init(rawValue: userVerification)
}
ASAuthorizationPublicKeyCredentialUserVerificationPreference
. Feels a bit like the German Donaudampfschiffahrtskapitänsmütze...The technical process by which an authenticator locally authorizes the invocation of the authenticatorMakeCredential and authenticatorGetAssertion operations.
ASAuthorizationController
and hand it over our assertionRequest
.// Pass in any mix of supported sign in request types.
let authController = ASAuthorizationController(authorizationRequests: [ assertionRequest ] )
authController.delegate = self
authController.presentationContextProvider = self
authController.performRequests()
AccountManager
class implements the ASAuthorizationControllerDelegate
interface. The official Apple documentation defines delegation as:Delegation is a design pattern that enables a class to hand off (or “delegate”) some of its responsibilities to an instance of another class.
performRequests()
method is called at the end, our own authorizationController()
function is being called, because we have delegated it to ourselves with the authController.delegate = self
two lines earlier. We have two versions of the authorizationController()
, one for the success case and one for the failure.func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {...}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {...}
authController.performRequests()
execution, right before our authorizationController()
is being called, the user has been presented with the prompt to unlock the Keychain, asking for Touch ID, Face ID, or the local device's PIN. In our case the passkey connected with the domain has been granted access to and was used to sign the challenge.ASAuthorization
object now and it contains the asserted credentials, stored in the credential property - it's an ASAuthorizationPlatformPublicKeyCredentialAssertion
in Apple API speak :D These asserted credentials are being sent to your web app's backend using the sendAuthenticationResponse()
function to verify them with the Hanko API.func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
...
switch authorization.credential {
...
case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion:
logger.log("A credential was used to authenticate: \(credentialAssertion)")
// After the server has verified the assertion, sign the user in.
sendAuthenticationResponse(params: credentialAssertion) {
self.didFinishSignIn()
}
...
}
didFinishSignIn()
function is being called, which in turn triggers the presentation of the screen for a signed-in user – today's Shiny!signInWith()
function is the viewDidAppear()
method of the SignInViewController
. So right at the start of our app it tries to sign in the user, using a passkey that is registered for your domain
. If there is no matching passkey on your iPhone or iPad, nothing visible happens. In our debug environment we can see an error message in the console in that case, courtesy to our logger.log()
call.signUpWith()
in the AccountManager
class. This function essentially takes a username, tries to register an account with our web app and subsequently creates a new passkey for it.signInWith()
function by creating a publicKeyCredentialProvider
:func signUpWith(userName: String, anchor: ASPresentationAnchor) {
self.authenticationAnchor = anchor
let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider(relyingPartyIdentifier: domain)
getRegistrationOptions(username: userName) /* ... */
}
getRegistrationOptions()
method defined in our AccountManager
class. It makes use of our web app's /registration_initialize
endpoint with the username as GET parameter:func getRegistrationOptions(username: String, ...) {
AF.request("https://\(domain)/registration_initialize?user_name=\(username)", method: .get). /* ... */
}
creationRequest
object. We are picking the challenge and user ID from it, converting them from Base64URL to Base64 in the process. We use both to create our actual credentials creation request, based on the publicKeyCredentialProvider
object:func signUpWith(userName: String, anchor: ASPresentationAnchor) {
/* ... */
getRegistrationOptions(username: userName) { creationRequest in
let challenge = creationRequest.publicKey.challenge.decodeBase64Url()!
let userID = creationRequest.publicKey.user.id.decodeBase64Url()!
let registrationRequest = publicKeyCredentialProvider.createCredentialRegistrationRequest(challenge: challenge,name: userName, userID: userID)
/* ... */
}
}
creationRequest
just like the challenge and user ID before and apply them to the registrationRequest
:getRegistrationOptions(username: userName) { creationRequest in
/* ... */
if let attestation = creationRequest.publicKey.attestation {
registrationRequest.attestationPreference = ASAuthorizationPublicKeyCredentialAttestationKind.init(rawValue: attestation)
}
if let userVerification = creationRequest.publicKey.authenticatorSelection?.userVerification {
registrationRequest.userVerificationPreference = ASAuthorizationPublicKeyCredentialUserVerificationPreference.init(rawValue: userVerification)
}
/* ... */
}
Attestation serves the purpose of providing a cryptographic proof of the authenticator attributes to the relying party in order to ensure that credentials originate from a trusted device with verifiable characteristics.
registrationRequest
we now proceed with the authController
like before during sign in.getRegistrationOptions(username: userName) { creationRequest in
/* ... */
let authController = ASAuthorizationController(authorizationRequests: [ registrationRequest ] )
authController.delegate = self
authController.presentationContextProvider = self
authController.performRequests()
}
authorizationController
to ourselves (see above), this time the switch catches on the ASAuthorizationPlatformPublicKeyCredentialRegistration
once the user has granted permission to create the new passkey. sendRegistrationResponse
has the web app verify and finalize the registration on the /authentication_finalize
endpoint and calls the app's didFinishSignIn()
on success.func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
let logger = Logger()
switch authorization.credential {
/* ... */
case let credentialRegistration as ASAuthorizationPlatformPublicKeyCredentialRegistration:
logger.log("A new credential was registered: \(credentialRegistration)")
sendRegistrationResponse(params: credentialRegistration) {
self.didFinishSignIn()
}
/* ... */
}
}