Skip to main content

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.

note

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 dashboard
  • user_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 device
  • purpose: the reason for generating a code. In this case it would always be temporary_authentication
  • authorize: 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.

note

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
}
note

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