One-Time Authentication
Some customers may want to log in on devices which either do not support FIDO, or a device which they do not own. In order to access this new device securely, a previously registered device is required to authorize the new device.
One-time authentication requires a backend API call, therefore it is recommended to leverage the LoginID server SDK. However, the LoginID APIs can be called directly if required.
It is best practice to have the end user attempt to log in on their new device, rather than give them an explicit option to say they are on a new device. After authentication is attempted, there will be an error indicating there are no credentials available on the device. Once this error is returned, the user should be prompted to either add this new device as an authenticator, or only use this device once. To add a new device as an authenticator, checkout the guide on adding an authenticator.
At a high level, this flow consists of three main components: generation of a code, authorization of that code, then using that code to login.
Prerequisites
- Create an application on LoginID Dashboard
- Download Client SDK
- Download Server SDK (optional)
With Server SDK
Step 1: Get user_id
The code generation functionality requires the user_id
rather than the username. In the case where you have not stored the user_id
, you can call the below SDK method to get the user_id
.
Server SDK: .getUserID(username)
The response is the user’s profile information (including the user_id
) for the provided username.
Step 2: Generate Code
Once you have the user_id, you must generate a code. This code is what ties the new device and existing device together. To understand which code_type is best for your use case, please see our guide on choosing a code type. The purpose should be temporary_authentication.
Server SDK: .generateCode(purpose, code_type, username, authorized)
This returns a code of the specified type as well as its expiry time.
Step 3: Wait for Authentication
After generating the code in Step 2, there are no further steps on the new device until the already registered device authorizes the request to login. This request is not returned until the authorization has been granted or the request has timed out (2 minutes). If this request times out, this request can be made again.
The authorization header is optional, therefore this call could be made directly from the frontend.
The scope of the authorization token is auth.temporary
Client SDK: .waitCode(user_id, code, code_type, credential_name)
Once the code has been authorized, this will return the jwt
as outlined in Step 6.
Step 4: Notify User
LoginID does not send the code to the user, as we understand it is important for you to maintain direct contact with your clients. Depending on your desired user experience, you can use the code returned in Step 2 in a variety of ways. For one-time authentication, this is frequently done through a QR code or push notification to the already registered device.
Step 5: Authorize Code
The code generated in Step 2 has to be authorized before being able to be used for one-time authentication. This should be done from an end user’s previously registered device. After the end user logs into their already registered device, they would be able to authorize the code.
Server SDK: .authorizeCode(code, purpose, code_type, user_id)
This returns confirmation the code was authorized, as well as the time the operation was completed.
Optional: Invalidate Code
It may be desired to provide the user the ability to deny the one-time authentication. If so, the code generated in Step 2 can be invalidated.
Server SDK: .invalidateAllCodes(purpose, code_type, user_id)
Step 6: Finish Flow
Once the end user has authorized the one-time authentication in Step 5, the .waitCode call made in Step 3 returns successfully and the user is authenticated.
Response:
{
"client": {
"id": "string",
"type": "directweb"
},
"credential": {
"uuid": "string",
"type": "fido2",
"name": "string"
},
"user": {
"id": "string",
"username": "string",
"namespace_id": "string"
},
"jwt": "string",
"is_authenticated": true
}
Where the jwt
is the authentication code in JWT format, same as would be returned after a normal successful authentication request
Calling APIs Directly
Step 1: Retrieve user_id
The code generation functionality requires the user_id rather than the username. In the case where you have not stored the user_id, you can call the below endpoint to get the user_id.
The required scope of the service token for this request is users.retrieve
Request:
POST /api/native/manage/users/retrieve
Header:
Authorization: Bearer {authorization_token}
Body:
{
"username": "string"
}
The response is the user’s profile information (including the user_id) for the provided username.
Step 2: Generate Code
Once you have the user_id, you must generate a code. This code is what ties the new device and existing device together. To understand which code_type is best for your use case, please see our guide on choosing a code type.
The scope of the authorization token is codes.generate
Request:
POST /api/native/codes/{type}/generate
Header:
Authorization: Bearer {authorization_token}
Body:
{
"client_id": <dashboard-generated client_id>,
"user_id": <user_id_to_be_authenticated>,
"purpose": "temporary_authentication",
"authorize": <bool>
}
Where:
client_id
: generated on the LoginID dashboarduser_id
: ID of an already registered user. In this case it would be the user_id of the person trying to authenticate on the new devicepurpose
: the reason for generating a code. In this case it would always be temporary_authenticationauthorize
: Whether the request to generate the code is pre-authorized. In this case it would be false as the new device has not yet been authorized
Response:
{
"code": <string>,
"expires_at": <ISO8061 timestamp>
"is_authorized": false
}
Step 3: Wait for Authentication
After generating the code in Step 2, there are no further steps on the new device until the already registered device authorizes the request to login. This request is not returned until the authorization has been granted or the request has timed out (2 minutes). If this request times out, this request can be made again.
The authorization header is optional, therefore this call could be made directly from the frontend.
The scope of the authorization token is auth.temporary
Request:
POST /api/native/authenticate/code/wait
Body:
{
"client_id": <dashboard-generated client_id>,
"username": <username>,
"authentication_code": {
"code": <code from Step 2 as string>,
"type": <type from Step 2 as string>
}
}
Step 4: Notify User
LoginID does not send the code to the user, as we understand it is important for you to maintain direct contact with your clients. Depending on your desired user experience, you can use the code returned in Step 2 in a variety of ways, such as email, QR code, SMS, or push notification.
Step 5: Authorize Code
The code generated in Step 2 has to be authorized before being able to be used to add the new authenticator. This should be done from an end user’s previously registered device. After the end user logs into their already registered device, they would be able to authorize the code.
The scope of the authorization token is codes.authorize
Request:
POST /api/native/codes/{type}/authorize
Authorization:
Authorization: Bearer {authorization_token}
Body:
{
"client_id": <dashboard-generated client_id>,
"user_id": <user_id to be authenticated>,
"purpose": "temporary_authentication",
"code": <user-entered code as string>
}
Response:
{
"expires_at": <ISO8061 timestamp>,
"is_authorized": true
}
The user_id
is returned in the JWT in the /authenticate/complete
call completed when the user logs in to their existing device, therefore the /manage/users/retrieve
should not be required to be called at this step.
Optional: Invalidate Code
It may be desired to provide the user the ability to deny the one-time authentication. If so, the code generated in Step 2 can be invalidated.
The scope of the authorization token is codes.invalidate
Request:
POST /api/native/codes/{code_type}/invalidate-all
Authorization:
Authorization: Bearer {authorization_token}
Body:
{
"client_id": <dashboard-generated client_id>,
"user_id": <user_id_to_be_authenticated>,
"purpose": "temporary_authentication"
}
Response:
{
"deleted_at": <ISO8061 timestamp>
}
Step 6: Finish Flow
Once the end user has authorized the one-time authentication in Step 5, the /api/native/authenticate/code/wait call made in Step 3 returns successfully and the user is authenticated.
Response:
{
"client": {
"id": "string",
"type": "directweb"
},
"credential": {
"uuid": "string",
"type": "fido2",
"name": "string"
},
"user": {
"id": "string",
"username": "string",
"namespace_id": "string"
},
"jwt": "string",
"is_authenticated": true
}
Where the jwt
is the authentication code in JWT format, same as would be returned after a normal successful authentication request