After the SDK has been personalized, authentication processes can be performed. Similar to a personalization an authentication is primarily started by scanning a QR Code or handling a Deep-Link. In addition, an authentication can also be initialized directly if the app acts as a service itself. This process is called InApp-Authentication. Normally the app / SDK only functions as "key" in the authentication process. Meaning it has no knowledge of the user's data, for example the user's attributes (email address, name, birthdate etc.) or the MappingId used by the XignIn-Manager. In the case of an InApp-Authentication the app can retrieve this data after a successful authentication. Furthermore, the authentication is also used in some utility process that need user confirmation which are referred in code as api access, e.g. change PIN or request a new activation. These processes also produce data with which an authentication process can be started directly from the app. For more information about how all these data can be acquired, please refer to the chapter Starting a process of the XignSys SDK.
The following diagram shows an overview over the authentication process, which is discussed in detail in this chapter.
An authentication can be started by invoking Authenticator.startAuthenticationSynchronous(data:)
with an instance of AuthenticationInitializationData. The result is an AuthenticateRequest object which contains an
Array/ aList of Role objects from which one must be used for the authentication. If the service is only configured
with one role, that particular role can be set directly. Otherwise, you should prompt the user to select a role. The
Role contains an Array<Array<AuthenticatorType>> / a List<List<AuthenticatorType>> of authenticators
named requestedAuthenticators. These reflect the factors that are used for singing the request later on, e.g. PIN or
biometrics. The selection is two-dimensional from which the first dimension represents alternatives and the second the
authenticators. The selection depends on the service's configuration. For example, if the service is configured to
either be signed by biometrics or PIN the resulting list can be illustrated like: [[biometric, device], [pin, device]]
. The following code snippet shows an example on how an authentication can be started:
internal func startAuthentication(
authenticationInitializationData: AuthenticationInitializationData
) throws {
let authenticator: Authenticator = XignSdk.shared.authenticator
// Submit the `AuthenticationInitializationData` to the XignIn-Manager in order to start
// an authentication.
let request: AuthenticateRequest = try authenticator.startAuthenticationSynchronous(
data: authenticationInitializationData
)
let roles: Array<Role> = request.roles
// Check if more than one `Role` is available. In that case a selection must be made.
if roles.count > 1 {
// Optional: Every `Role` contains a list with optional authenticator constellations with
// which the request must be signed later on. These can be displayed to the user in order
// to help the user to make a choice.
// The following example show how type can be accessed:
let authenticators: Array<Array<AuthenticatorType>>? = roles.first?.requestedAuthenticators
// Let the user select a role.
let selectedRole: Role = YourImplementation.displayRoleSelection(roles)
if request.setSelection(selectedRole) == nil {
// This function returns the set role. It returns `nil` in case `nil` is passed or a role
// that is not part of the given selection `request.roles`. This is only relevant for
// development and can be ignored during production, if implemented correctly.
}
}
// Continue the process by checking a confirming the users attributes.
// Note: This is a call to the next documentation example function that illustrates the
// attribute confirmation.
try confirmAuthenticationAttributes(request: request)
}
internal fun startAuthentication(authenticationInitializationData: AuthenticationInitializationData) {
val authenticator: Authenticator = XignSdk.shared.authenticator
// Submit the `AuthenticationInitializationData` to the XignIn-Manager in order to start
// an authentication.
val request: AuthenticateRequest = authenticator.startAuthenticationSynchronous(
authenticationInitializationData
)
val roles: List<Role> = request.roles
// Check if more than one `Role` is available. In that case a selection must be made.
if (roles.size > 1) {
// Optional: Every `Role` contains a list with optional authenticator constellations with
// which the request must be signed later on. These can be displayed to the user in order
// to help the user to make a choice.
// The following example show how type can be accessed:
val authenticators: List<List<AuthenticatorType>>? = roles.firstOrNull()?.requestedAuthenticators
// Let the user select a role.
val selectedRole: Role = YourImplementation.displayRoleSelection(roles)
if (request.setSelection(selectedRole) == null) {
// This function returns the set role. It returns `null` in case `null` is passed or a role
// that is not part of the given selection `request.roles`. This is only relevant for
// development and can be ignored during production, if implemented correctly.
}
}
// Continue the process by checking a confirming the users attributes.
// Note: This is a call to the next documentation example function that illustrates the
// attribute confirmation.
confirmAuthenticationAttributes(request = request)
}
Note: As of SDK version 4.1.0 the functions within the authentication process can throw an error, if the security level of the device/app is not sufficient. The
XignSdkSecurityInsufficientError(XignSdkSecurityInsufficientException) should be handled. For more information see chapter security check.
After the role has been selected the authentication process continues by confirming the attributes. For that purpose the
AuthenticateRequest contains an AuthenticationPersonalInformation property named info. This object carries
information about the user's attributes that must be confirmed for the authentication, for example the email address,
given name, family name etc. This process is needed for services that request these attributes directly from the
XignIn-Manager. However, the XignIn-Manager is a complex system that can be configured in many ways and the storage of
user data is one of the components that is variable. It is possible to create services that are only used for
authentication purposes and do not request any user data from the XignIn-Manager. For example, a third-party identity
manager such as a Keycloak can be used to manage the user data. In this case, the XignIn-Manager only provides a
mapping id with which the application can fetch the user data after a successful authentication from the third-party
identity manager. Consequently, the service configured within the XignIn-Manager has no knowledge about which attributes
are requested from the user and therefore no attributes must be confirmed within the SDK. This said, the confirmation of
user attributes can be skipped, if the implementing app only supports services that do not require user data directly
from the XignIn-Manager.
Note: The XignIn-Manager of the Servicekonto.NRW does not hold any user data directly. Therefore, the confirmation of user attributes within the SDK can be skipped.
The AuthenticationPersonalInformation contains properties of the type AuthenticationInformation named after each
possible attribute, e.g. givenName, email, birthdate etc. Each of these properties must be treated in a similar
manner. First, if a service does not request personal information of a specific type the corresponding property
is nil(null) and does not need further handling. Otherwise, the AuthenticationInformation determines how the
attribute must be handled in detail. Generally the XignIn-Manager supports optional attributes, i.e. attributes that the
service does not require for core functionalities but for optional additional features the user can opt in to. However,
optional attributes are not yet fully supported and can be ignored for now. Required attributes on the other hand must
always be confirmed, otherwise the authentication can not be completed. If the user does not want to confirm these, the
process should be aborted.
Services may require personal information the user has not yet provided. Since authentication processes should not be
needed to be canceled in this case, XignIn supports adding missing information within the process. Therefore, the
property isDataMissing must be checked. If personal information are missing the user must be asked for the
corresponding data. The resulting input can be set by invoking setValue(value:). The accepted type is determined by
the attribute, e.g. for the givenName a String can be passed and for the birthdate an instance
of Date (Instant). The SDK roughly validates the input and returns an enum which indicates whether the value was
accepted or which error occurred. Depending on the attribute, a different implementation is returned, however all share
a case named valid (VALID). In this case the supplied value was accepted. Any other case signals an invalid input
and the stored value is reset to nil (null). For easier implementation of the UI the attributes validator can be
accessed directly via the property validator and may provide additional parameters like minLength or maxLength.
Contrary to missing personal information, an attribute can also have several values. In this case the user must choose
which value should be supplied to the service. For data privacy reasons the concrete values to choose from are not
returned, instead aliases are used which can be checked via the property aliases. This property is a set
of PersonalInformationAlias. If more than one entry is present a selection must be made. An instance
of PersonalInformationAlias contains the displayable alias as String the user has configured for the value and a
flag named isPrimary. The flag can be used to offer the user an easier selection process by for example prefilling the
UI component used for the selection with the primary alias. The chosen alias can be set by invoking setSelection on
the AuthenticationInformation instance. This function returns the adopted value or nil (null), in case an alias
was passed which is not part of the available selection.
Note: In most cases a selection of an attribute value only needs to be done once per service. After the first successful authentication against a service the selection is saved by the XignIn-Manager. All further authentications against the same service will only request the previous selected values. If that is the case, the
AuthenticationInformation.aliasesset contains only one entry, which will be automatically selected.
If neither personal information are missing nor an alias must be selected the attribute simply needs to be confirmed. For required attributes no further steps are required at this point in the authentication process. However, it is strongly recommended that the user is presented with at least some kind of UI that indicates what personal information will be transmitted to the service after the authentication process is completed.
After all stored properties within the AuthenticationPersonalInformation have been handled, the user information can
be prepared for signing. This can be achieved by
invoking Authenticator.prepareAuthenticationUserInformationSynchronous(:) with the AuthenticateRequest. The result
of that function is an enum (sealed class) of the type PrepareAuthenticationUserInformationResult with two cases (
concrete implementations) failure and success.
The failure case indicates that at least one of the requested attributes is erroneous. In order to correct the errors
the previous filled out AuthenticateRequest with updated error information is returned. For that purpose
every AuthenticationInformation property within the AuthenticationPersonalInformation contains a field named error
. This field is of the type AuthenticationInformationErrorCode which is an enum describing the error. The
case none (NONE) indicates that attribute is errorless and can be skipped. Any other case however is an error state
and must be handled according to its documentation, which mostly leads to displaying the error to the user and request a
new input. The new input can be supplied to the AuthenticationInformation by the same means as initially discussed.
After all errors haven be corrected the process can be repeated by
invoking Authenticator.prepareAuthenticationUserInformationSynchronous(:) again with the AuthenticateRequest.
Note: The correction of errors cannot be repeated indefinitely. After a certain number of retries a
XignSdkError(XignSdkException) is raised, which cancels the current authentication process.
In the success case an instance of AuthenticationUserInformation is returned, with which the process can be
continued. The following example illustrates how the discussed confirmation of user attributes can look like in code:
private func confirmAuthenticationAttributes(
request: AuthenticateRequest
) throws {
let info: AuthenticationPersonalInformation = request.info
// The info object contains information about the requested attributes of
// the user that must be confirmed. If attribute data are missing, they must
// be updated within this object. The next step is only relevant if the services
// or XignIn-Manager you are targeting manages user information, otherwise it can be
// skipped.
// The following example shows how `AuthenticationInformation` can be handled using
// the attribute `givenName`:
// Firstly check if the property is not `nil`. If it is `nil`, it is not needed
// and can be skipped.
if let infoGivenName: AuthenticationInformation<String, NameValidator> = info.givenName {
// Secondly check if it is required. If not, it can be skipped.
if infoGivenName.isRequired {
// Thirdly check if data are missing.
// Note: Some attributes can't be supplied during an authentication as
// they need some kind of verification.
if infoGivenName.isDataMissing {
var isInputValid: Bool = false
var errorText: String? = nil
while !isInputValid {
// Query the user for input.
let suppliedGivenName: String = YourImplementation.askTheUserForData(
minLength: infoGivenName.validator.minLength,
maxLength: infoGivenName.validator.maxLength,
errorText: errorText
)
// Try setting the given input to the object.
let validationResult: NameValidator.ResultCode = infoGivenName
.setValue(value: suppliedGivenName)
// Check if the input was accepted by the object.
// Note: If the input is not valid, the input within the object is cleared.
switch validationResult {
case .valid:
// everything is fine, continue the process.
isInputValid = true
case .empty:
// The input is `nil` or empty; display an error and repeat.
errorText = "The input must not be empty."
case .tooShort:
// The input is too short. Note for most fields the min length is
// only 1 character.
errorText = """
The input must be at least \
\(infoGivenName.validator.minLength) characters long.
"""
case .tooLong:
// The input is too long.
errorText = """
The input can't be longer than \
\(infoGivenName.validator.maxLength) characters.
"""
}
}
}
// If data are not missing, check if alias are available for selection.
// Note: If only one alias is available, it is set automatically.
else if infoGivenName.aliases.count > 1 {
// Let the user select an alias.
let alias: PersonalInformationAlias = YourImplementation
.displayAliasSelection(infoGivenName.aliases)
// Supply the selected alias to `infoGivenName` object.
if infoGivenName.setSelection(alias) == nil {
// This function returns the set alias. It returns nil in case `nil` is passed or
// an alias that is not part of the given selection `infoGivenName.aliases`. This
// is only relevant for development and can be ignored during production, if
// implemented correctly.
}
} else {
// In this case, the attribute is required but does not need further input. However,
// it is recommended to at least notify the user that the service will receive
// this piece of information.
}
} else {
// Currently, there are no optional attributes. Therefore, this case
// can be left empty.
}
}
// Repeat the code form above in a similar matter for every other property of the `info`
// variable. The following list show all other properties that must be handled:
info.username; info.nickname; info.familyName; info.birthdate; info.address; info.nationality
info.placeOfBirth; info.gender; info.salutation; info.email; info.phoneNumber
// Submit the previous supplemented `AuthenticateRequest` object to the XignIn-Manager.
let result: PrepareAuthenticationUserInformationResult =
try XignSdk.shared.authenticator.prepareAuthenticationUserInformationSynchronous(
request: request
)
let userInformation: AuthenticationUserInformation
switch result {
case .failure(let returnedAuthenticateRequest):
// Correct the errors contained in the `returnedAuthenticateRequest` and repeat the
// preparation.
// The following example shows how errors can be checked using the attribute `givenName`:
let infoGivenNameOpt: AuthenticationInformation<String, NameValidator>? =
returnedAuthenticateRequest.info.givenName
if let infoGivenName = infoGivenNameOpt {
let error: AuthenticationInformationErrorCode = infoGivenName.error
switch error {
case .none:
// no error
break
case .invalid:
// Indicates that the data provided for this attribute are in an invalid format.
break
case .notAvailable:
// Indicates that attribute data was supplied, although it has not been required.
break
case .dataMissing:
// Indicates that data for this attribute must be supplied but has not been supplied.
break
case .noAliasSelected:
// Indicates that and aliases must be selected for this attribute but none was selected.
break
}
}
// After identifying all errors contained in the `returnedAuthenticateRequest`, present them
// to the user, request new input to fix the errors and then repeat the process.
// Note: This process is simplified in this example by invoking the current function again.
try confirmAuthenticationAttributes(request: returnedAuthenticateRequest)
return
case .success(let infos):
userInformation = infos
}
// After supplying all needed data the authentication can be continued by signing to data.
// Note: This is a call to the next documentation example function that illustrates the singing.
try signAuthentication(userInformation: userInformation)
}
private fun confirmAuthenticationAttributes(
request: AuthenticateRequest
) {
val info: AuthenticationPersonalInformation = request.info
// The info object contains information about the requested attributes of
// the user that must be confirmed. If attribute data are missing, they must
// be updated within this object. The next step is only relevant if the services
// or XignIn-Manager you are targeting manages user information, otherwise it can be
// skipped.
// The following example shows how `AuthenticationInformation` can be handled using
// the attribute `givenName`:
// Firstly check if the property is not `nil`. If it is `nil`, it is not needed
// and can be skipped.
val infoGivenName = info.givenName
if (infoGivenName != null) {
// Secondly check if it is required. If not, it can be skipped.
if (infoGivenName.isRequired) {
// Thirdly check if data are missing.
// Note: Some attributes can't be supplied during an authentication as
// they need some kind of verification.
if (infoGivenName.isDataMissing) {
var isInputValid: Boolean = false
var errorText: String? = null
while (!isInputValid) {
// Query the user for input.
val suppliedGivenName: String = YourImplementation.askTheUserForData(
minLength = infoGivenName.validator.minLength,
maxLength = infoGivenName.validator.maxLength,
errorText = errorText
)
// Try setting the given input to the object.
val validationResult: NameValidator.ErrorCode = infoGivenName
.setValue(value = suppliedGivenName)
// Check if the input was accepted by the object.
// Note: If the input is not valid, the input within the object is cleared.
when (validationResult) {
NameValidator.ErrorCode.VALID -> {
// everything is fine, continue the process.
isInputValid = true
}
NameValidator.ErrorCode.EMPTY -> {
// The input is `null` or empty; display an error and repeat.
errorText = "The input must not be empty."
}
NameValidator.ErrorCode.TOO_SHORT -> {
// The input is too short. Note for most fields the min length is
// only 1 character.
errorText = """
The input must be at least
${infoGivenName.validator.minLength} characters long.
""".trimIndent()
}
NameValidator.ErrorCode.TOO_LONG -> {
// The input is too long.
errorText = """
The input can't be longer than
${infoGivenName.validator.maxLength} characters.
""".trimIndent()
}
}
}
}
// If data are not missing, check if alias are available for selection.
// Note: If only one alias is available, it is set automatically.
else if (infoGivenName.aliases.size > 1) {
// Let the user select an alias.
val alias: PersonalInformationAlias = YourImplementation
.displayAliasSelection(infoGivenName.aliases)
// Supply the selected alias to `infoGivenName` object.
if (infoGivenName.setSelection(alias) == null) {
// This function returns the set alias. It returns `null` in case `null` is passed
// or an alias that is not part of the given selection `infoGivenName.aliases`.
// This is only relevant for development and can be ignored during production, if
// implemented correctly.
}
} else {
// In this case, the attribute is required but does not need further input. However,
// it is recommended to at least notify the user that the service will receive
// this piece of information.
}
} else {
// Currently, there are no optional attributes. Therefore, this case
// can be left empty.
}
}
// Repeat the code form above in a similar matter for every other property of the `info`
// variable. The following list show all other properties that must be handled:
info.username; info.nickname; info.familyName; info.birthdate; info.address; info.nationality
info.placeOfBirth; info.gender; info.salutation; info.email; info.phoneNumber
// Submit the previous supplemented `AuthenticateRequest` object to the XignIn-Manager.
val result: PrepareAuthenticationUserInformationResult =
XignSdk.shared.authenticator.prepareAuthenticationUserInformationSynchronous(
request = request
)
val userInformation: AuthenticationUserInformation
when (result) {
is PrepareAuthenticationUserInformationResult.Failure -> {
val returnedAuthenticateRequest: AuthenticateRequest = result.request
// Correct the errors contained in the `returnedAuthenticateRequest` and repeat the
// preparation.
// The following example shows how errors can be checked using the attribute `givenName`:
val infoGivenName: AuthenticationInformation<String, NameValidator.ErrorCode, NameValidator>? =
returnedAuthenticateRequest.info.givenName
if (infoGivenName != null) {
val error: AuthenticationInformation.ErrorCode = infoGivenName.error
when (error) {
AuthenticationInformation.ErrorCode.NONE -> {
// no error
}
AuthenticationInformation.ErrorCode.INVALID -> {
// Indicates that the data provided for this attribute are in an invalid format.
}
AuthenticationInformation.ErrorCode.NOT_AVAILABLE -> {
// Indicates that attribute data was supplied, although it has not been required.
}
AuthenticationInformation.ErrorCode.DATA_MISSING -> {
// Indicates that data for this attribute must be supplied but has not been supplied.
}
AuthenticationInformation.ErrorCode.NO_ALIAS_SELECTED -> {
// Indicates that and aliases must be selected for this attribute but none was selected.
}
}
}
// After identifying all errors contained in the `returnedAuthenticateRequest`, present them
// to the user, request new input to fix the errors and then repeat the process.
// Note: This process is simplified in this example by invoking the current function again.
confirmAuthenticationAttributes(request = returnedAuthenticateRequest)
return
}
is PrepareAuthenticationUserInformationResult.Success -> {
userInformation = result.userInformation
}
}
// After supplying all needed data the authentication can be continued by signing to data.
// Note: This is a call to the next documentation example function that illustrates the singing.
signAuthentication(userInformation = userInformation)
}
After all requested attributes have been confirmed as discussed in the previous chapter the signing step of the
authentication process can be commenced with an instance of AuthenticationUserInformation. During the signing the
identity of the user is determined by signing all requested attributes with the requested authenticators. Like the
attributes the authenticators requested can be configured within the service on the XignIn-Manager. Depending on the
configuration different authenticator combinations must be handled. For that purpose the AuthenticationUserInformation
contains a property named requestedAuthenticators which is a two-dimensional list of the enum AuthenticatorType. The
inner lists (the second dimension) represents mandatory combinations of factor types to be used in conjunction, the
outer list (first dimension) stands for alternatives of these conjunctions. For example, given the
list [[.pin, .device], [.biometric, .device]] the first inner list of [.pin, .device] requires the user to provide
both the PIN and the device factor, if this entry is chosen. On the other hand, the second inner list
of [.biometric, .device] offers the user to alternatively authenticate by providing both the biometric and the device
factor. In contrast, providing only a PIN factor or for example a combination of a PIN factor and a biometric factor
will fail to meet the specified requirements and therefore, the user can not authenticate successfully. Furthermore, the
list of requestedAuthenticators will only contain combination the client should be able to supply. For example, if a
service is configured to allow biometric authentication but the app did not enroll biometrics within the XignSys SDK,
the requestedAuthenticators will not contain an entry with biometrics. However, if the app does not match any possible
combination, a list containing all configured combinations is returned, so the app can notify the user about which
factors have to be enrolled to authenticate against the service.
To sign the AuthenticationUserInformation a list from the requestedAuthenticators must be chosen from which every
authenticator must be supplied. Authenticators are supplied in form of an instance of AuthenticationFactor. With such
an instance the function sign(factor:) of the AuthenticationFactor can be invoked. Because authenticators may need
different data a more specific implementation exists for every authenticator which class name matches the case name of
the AuthenticatorType.
Currently, only three authenticators are fully implemented and can occur. The more specific AuthenticationFactor
classes are: DeviceFactor, PinFactor and BiometricFactor. A DeviceFactor can simply be created by invoking its
initializer (constructor). For the PinFactor the users activation PIN is required, with which the initializer (
constructor) of the object can be invoked. The creation of a BiometricFactor
needs a bit more handling, because the platforms biometric API must be used to locally authenticate the user in order to
create the factor. As this process is the same as for personalisation, it is discussed in detail in
chapter Creating a BiometricFactor. Note that, the creation of a BiometricFactor can
fail if the system managed biometric patterns (e.g. saved fingerprints) have been modified after the enrollment within
the XignSys SDK. In this case another combination could be supplied from requestedAuthenticators that does not require
biometrics. If that is not possible the authentication end at this point, because the required authenticators can not be
supplied.
Note: Prior to SDK version 4.0.0 the signing with a
PinFactorthrew an error, if the wrong PIN was supplied. This is not the case anymore. As for newer versions the PIN is only validated on the XignIn-Manager and produces a lockout, if the wrong PIN is supplied.
After the AuthenticationUserInformation object has been signed with all requested authenticators the authentication
can be finished. The following code shows an example on how the signing can be implemented:
private func signAuthentication(userInformation: AuthenticationUserInformation) throws {
// Retrieve the list of authenticators that must be supplied.
let requestedAuthenticators: Array<Array<AuthenticatorType>> =
userInformation.requestedAuthenticators
// Select a set of authenticators to provide, either automatically or let the user decide.
let authenticators: Array<AuthenticatorType> = requestedAuthenticators.first!
// Iterate over the selected authenticators and sign the userInformation respectively.
for authenticator in authenticators {
let factor: AuthenticationFactor
switch authenticator {
case .device:
// Just create a new instance of the `DeviceFactor` class.
factor = DeviceFactor()
case .pin:
// Query the user for the PIN.
let pin: String = YourImplementation.askUserForPin()
factor = PinFactor(pin)
case .biometric:
// Query the user for biometrics and create a `BiometricFactor`. For overview
// purposes this process is substituted by another function call.
let biometricFactor: BiometricFactor = try YourImplementation.getBiometricFactor()
factor = biometricFactor
case .otp, .yubikey:
// Handling for factors that are currently in development and not fully functional
throw YourError.yourErrorCase
}
// Sign the `userInformation` object with the factor.
try userInformation.sign(factor: factor)
}
// Continue the process with the signed userInformation object.
// Note: This is a call to the next documentation example function that illustrates the
// final step of the authentication process.
try finishAuthentication(userInformation: userInformation)
}
private fun signAuthentication(userInformation: AuthenticationUserInformation) {
// Retrieve the list of authenticators that must be supplied.
val requestedAuthenticators: List<List<AuthenticatorType>> =
userInformation.requestedAuthenticators
// Select a set of authenticators to provide, either automatically or let the user decide.
val authenticators: List<AuthenticatorType> = requestedAuthenticators.first()
// Iterate over the selected authenticators and sign the userInformation respectively.
authenticators.forEach { authenticator ->
val factor: AuthenticationFactor
when (authenticator) {
AuthenticatorType.DEVICE -> {
// Just create a new instance of the `DeviceFactor` class.
factor = DeviceFactor()
}
AuthenticatorType.PIN -> {
// Query the user for the PIN.
val pin: String = YourImplementation.askUserForPin()
factor = PinFactor(pin)
}
AuthenticatorType.BIOMETRIC -> {
// Query the user for his biometrics and create a `BiometricFactor`. For overview
// purposes this process is substituted by another function call.
val biometricFactor: BiometricFactor = YourImplementation.getBiometricFactor()
factor = biometricFactor
}
AuthenticatorType.OTP,
AuthenticatorType.YUBIKEY -> {
// Handling for factors that are currently in development and not fully functional
throw YourException()
}
}
// Sign the userInformation object with the factor.
userInformation.sign(factor)
}
// Continue the process with the signed userInformation object.
// Note: This is a call to the next documentation example function that illustrates the
// final step of the authentication process.
finishAuthentication(userInformation = userInformation)
}
The authentication process can be finished by invoking Authenticator.finishAuthentication(userInfo:)
with the signed AuthenticationUserInformation. The result is an enum (a sealed class in Kotlin). The next code
example shows how this step can be implemented; the cases are explained afterwards.
private func finishAuthentication(userInformation: AuthenticationUserInformation) throws {
let authenticator = XignSdk.shared.authenticator
// Submit the previous signed `AuthenticationUserInformation` object to the XignIn-Manager.
let result: AuthenticationResult = try authenticator.finishAuthenticationSynchronous(
userInfo: userInformation
)
switch result {
case .userLogin(let userLoginResult):
// The end of a normal user login. The userLoginResult contains some meta information.
return
case .serviceLogin(let serviceLoginResult):
// The end of the authentication during an InApp-Authentication process. The
// serviceLoginResult is needed to request the user's attribute data as well as
// the MappedId.
// TODO: finish the inApp-Authentication
return
case .apiAccessAddFactor(let apiAccessAddFactor):
// A special result needed to subsequently add a factor, needs further handling.
return
case .apiAccessChangePin(let apiAccessChangePin):
// A special result needed to change the user's PIN, needs further handling.
return
case .apiAccessMergeComplete(idmIdentifier: let idmIdentifier):
// Signalizes the success of an account merge.
return
case .apiAccessRequestNewActivationData(let apiAccessRequestNewActivationData):
// A special result needed to request a new activation, needs further handling.
return
case .apiAccessDelete(let apiAccessDelete):
// An `apiAccessDelete` indicates the successful authentication in order to
// perform a delete action, needs further handling.
return
case .lockout(
lockoutInformation: let lockoutInformation,
authenticationUserInformation: let authenticationUserInformation
):
// Happens, if one of the supplied authenticators was invalid, e.g. wrong PIN.
// In this case the lockout duration within `lockoutInformation` must be
// waited. After that all required factors must be supplied again and the
// process can be retried.
// Check if the lockout is permanent.
if lockoutInformation.isPermanent {
// The activation has been disabled permanently, notify the user an cancel the process.
throw YourError.yourErrorCase
}
// Retrieve the date until which the lock is effective.
let lockedUntil: Date = lockoutInformation.lockedUntil
// Wait until the lock has been lifted.
// Retry the signing of the `authenticationUserInformation`.
// Note: This is a call to the previous documentation example function that
// illustrates the singing.
try signAuthentication(userInformation: authenticationUserInformation)
return
}
}
private fun finishAuthentication(userInformation: AuthenticationUserInformation) {
val authenticator: Authenticator = XignSdk.shared.authenticator
// Submit the previous signed `AuthenticationUserInformation` object to the XignIn-Manager.
val result: AuthenticationResult = authenticator.finishAuthenticationSynchronous(
userInfo = userInformation
)
when (result) {
is UserLoginResult -> {
// The end of an normal user login. The userLoginResult contains some meta information.
return
}
is ServiceLoginResult -> {
// The end of the authentication during an InApp-Authentication process. The
// serviceLoginResult is needed to request the user's attribute data as well as
// the mapping Id.
// TODO: finish the inApp-Authentication
return
}
is ApiAccessAddFactor -> {
// A special result needed to subsequently add a factor, needs further handling.
return
}
is ApiAccessChangePin -> {
// A special result needed to change the user's PIN, needs further handling.
return
}
is ApiAccessMergeComplete -> {
// Signalizes the success of an account merge.
return
}
is ApiAccessRequestNewActivationData -> {
// A special result needed to request a new activation, needs further handling.
return
}
is ApiAccessDelete -> {
// An `apiAccessDelete` indicates the successful authentication in order to
// perform a delete action, needs further handling.
return
}
is Lockout -> {
val lockoutInformation: LockoutAttemptInformation = result.lockoutInformation
val authenticationUserInformation: AuthenticationUserInformation =
result.authenticationUserInformation
// Happens, if one of the supplied authenticators was invalid, e.g. wrong PIN.
// In this case the lockout duration within `lockoutInformation` must be
// waited. After that all required factors must be supplied again and the
// process can be retried.
// Check if the lockout is permanent.
if (lockoutInformation.isPermanent) {
// The activation has been disabled permanently,
// notify the user and cancel the process.
throw YourException()
}
// Retrieve the date until which the lock is effective.
val lockedUntil: Instant = lockoutInformation.lockedUntil
// Wait until the lock has been lifted.
// Retry the signing of the `authenticationUserInformation`.
// Note: This is a call to the previous documentation example function that
// illustrates the signing.
signAuthentication(userInformation = authenticationUserInformation)
return
}
}
}
As mentioned in the beginning of the Authentication chapter the authentication process is also used
by various other processes, e.g. change PIN or add factor, therefore the final result of the authentication process
changes depending on how the processes was started. Some of these results mark the end of the process, others however
need further handling as mentioned within the inline comments in the previous examples. The implementation of the result
object differ a little between Swift and Kotlin. In Swift most enum cases contains one parameter which is an instance of
the type that is needed to continue the process, whereas in Kotlin the case is the type. For example, the parameter of
the case AuthenticationResult.apiAccessChangePin(:) is of the type ApiAccessChangePin and in Kotlin the concrete
subclass of the AuthenticationResult is of the type AuthenticationResult.ApiAccessChangePin. For enum cases that
provided more than one parameter or the parameter's type is of a standard class (e.g. String), the Kotlin
implementation is a wrapper object that contains these parameters as properties. In the following, all cases are
discussed in detail using the previous Swift example:
The case .userLogin(let userLoginResult) marks the end of simple user authentication. It can be expected when the
authentication has been started by scanning a QR code or handling a Deep-Link as described in the
chapters QR code and Deep-Links.
No further handling is required in this case. The returned parameter of the type UserLoginResult contains some
metadata, e.g. relyingPartyName - the name of the service, relyingPartyUrl - the url of the service, roleName -
the used role etc., that optionally can be displayed to the user or processed otherwise.
The case .serviceLogin(let serviceLoginResult) is part of the InApp-Authentication. It can be expected when the
authentication has been started by Authenticator.fetchInAppAuthenticationInitializationData(:) as described in the
chapter Starting an InApp-Authentication. This case contains
an instance of ServiceLoginResult which is needed to fetch the OIDC authorization code. Therefore, the started
process is not finished yet and must be continued by invoking Authenticator.fetchAuthorizationCode(:). For further
details please refer to the chapter Finishing an InApp-Authentication.
The case .apiAccessAddFactor(let apiAccessAddFactor) is returned when the authentication has been started with the
result of the function Personalizer.startAddAuthenticationFactor(:) to add a factor to the current
personalization, e.g. biometric. The returned parameter ApiAccessAddFactor is needed to continue the process. This
object needs further handling before is can be used to finish the process. Please take a look at the
chapter Add factor for that purpose.
The case .apiAccessChangePin(let apiAccessChangePin) can be expected if the authentication has been started in order
to change the PIN afterwards. The returned parameter ApiAccessChangePin needs to be supplied with the new PIN
before invoking Personalizer.finishChangePin(request:) to finish the process. For more information please refer to the
chapter Change PIN.
The case .apiAccessMergeComplete(idmIdentifier: let idmIdentifier) signalizes the end of a successful account merge in
the scenario of an organization invite. This result can be expected if the authentication has been started with
the AuthenticationInitializationDataWithSession from PersonalizationResponse.mergePersonalization(::) of the
personalization process as described in the
chapter Decrypting the PersonalizationInitializationData. After
receiving this result the user should be informed that the activation QR code from the organization invite email has
become invalid and can be discarded. Otherwise, no further action is required.
The case .apiAccessRequestNewActivationData(let apiAccessRequestNewActivationData) is part of the request a new
activation process. It can be expected if the authentication has been started with the result of the
function Personalizer.startRequestNewActivationData(idmIdentifier:). The parameter of the
type ApiAccessRequestNewActivationData is needed to continue the process. It must be supplied with a new activation
PIN and optionally with an alias for the new activation before the process can be finished by invoking
Personalizer.finishRequestNewActivationDataSynchronous(:). For more information please take a look at the chapter
Request a new Activation.
The case .apiAccessDelete(let apiAccessDelete) indicates a successful authentication in order to perform a delete
action. It can be expected if the authentication has been started with the result of the function
Personalizer.startDeletetion(idmIdentifier). The returned parameter ApiAccessDelete needs to be supplied to
Personalizer.finishDeletion(request:) in order to finish the process. Further information can be found in the chapter
Delete Account.
Finally, the last case .lockout(lockoutInformation:authenticationUserInformation:) is a special case and can always
happen. It does not depend on how the process was started, but on whether the supplied signatures of
the AuthenticationUserInformation are valid or not. Similar to the personalization process this can happen, if for
example the wrong PIN was given by the user. In this case two objects are returned, the LockoutInformation and
the AuthenticationUserInformation. The LockoutInformation contains a period of time the user must wait until the
process can be repeated. For more information about the lockout handling, please refer to the
chapter Lockout Handling. If possible, the process can be repeated from the previous step
Signing of the authentication request with the
given AuthenticationUserInformation after the lockout duration has passed. Note that all previous supplied signatures
haven been cleared, because at least one of them was invalid. For security reasons, the exact authenticator that was
invalid cannot be disclosed.