1.0 Installation
Create a file .npmrc in your project root directory and add the following line:
registry=https://npm.pantherx.org
Install the library with npm or pnpm:
npm install @ones.dev/common
npm install @ones.dev/backend-modules
Important: To use deviceIdentitySigningFunction you need to register your device with IDP first, and make sure that px-device-identity-service is running. See here.
2.0 Device Authentication
Access px-device-identity service to sign the device JWT.
import { AuthJwtProperties, TokenIntrospectionResponse, unixTimeInSeconds } from '@ones.dev/common'
import { DeviceAuthentication, deviceIdentitySigningFunction, loadSystemConfig } from '@ones.dev/backend-modules'
// Load system config; defaults to /etc/px-device-identity/device.yml
const {
deviceId,
clientId,
authServerUrl
} = loadSystemConfig()
// Initialize the device authentication
const auth = new DeviceAuthentication({
name: 'device-auth',
idpBaseUrl: authServerUrl,
deviceId: deviceId,
deviceClientId: clientId,
functions: {
// Sign using px-device-identity service
signingFunction: deviceIdentitySigningFunction
},
deviceIdentityUrl: "http://127.0.0.1:8000/sign",
authJwtProperties: new AuthJwtProperties({
scope: 'openid offline_access ones',
bindingMessage: `Authorize login to Example.com`,
resources: ['https://example.com'],
}),
})
// Get the device JWT
const deviceJwt = await auth.makeDeviceJwt()
// Get the device access token
const { access_token, expires_in, token_type } = await auth.makeDeviceAccessToken()
// Do access token introspection
const { active, sub, ... } = await auth.tokenIntrospection(access_token)
3.0 CIBA
Implement CIBA flow on server side, for OnesID users to authenticate with their username (OnesID).
3.1 Make CIBA Request
On your backend, make a CIBA request to authenticate the user.
// Reuse the device authentication from the previous example
const auth = new DeviceAuthentication({ ... })
// Make the CIBA request
const {
auth_req_id,
experies_in,
interval,
} = await auth.makeCibaRequest({
loginHint: {
kind: loginHintKind,
value: userIdentifier,
}
})
3.1.1 Example
On a NestJS backend, this looks something like this:
@Public()
@Post('/api/auth/oidc/bc-authorize')
async cibaLoginRequest(@Body() body: CibaAuthRequestFrontendDto): Promise<CibaAuthRequestResponseDto> {
return auth.makeCibaRequest({...})
}
3.1.2 Frontend usage
If your CIBA request endpoint is /api/auth/oidc/bc-authorize, then you can use the following code on the frontend:
import { makeCibaRequestClient } from '@ones.dev/common'
const host = 'https://example.com'
const userIdentifier = 'user.name@OnesID1'
const { auth_req_id, expires_in, interval } = await makeCibaRequestClient(host, userIdentifier)
3.2 Check CIBA Status
Poll the CIBA status in 3s interval until it is approved, or denied.
// Reuse the device authentication from the previous example
const auth = new DeviceAuthentication({ ... })
try {
const result = await auth.checkCibaStatus(auth_req_id)
return result
} catch (err) {
const axiosErr = err as AxiosError
// Usually indicates that the request is pending
if (axiosErr?.response?.status === 400) {
throw new BadRequestException(axiosErr.response.data)
} else {
throw new NotFoundException()
}
}
// Validate returned JWT (access_token, id_token)
const {
sub,
iss,
aud,
exp,
iat,
...
} = await auth.validateJwt(access_token)
3.2.1 Example
On a NestJS backend, this looks something like this:
@Public()
@Post('/api/auth/oidc/token')
async tokenEndpoint(
@Body() body: CibaAuthStatusRequestFrontendDto,
): Promise<CibaAuthStatusResponseDto> {
try {
const result = await this.service.cibaStatus(body.authRequestId)
return result
} catch (err) {
const axiosErr = err as AxiosError
this.logger.error(axiosErr)
if (axiosErr?.response?.status === 400) {
throw new BadRequestException(axiosErr.response.data)
} else {
this.logger.error(axiosErr?.response)
throw new NotFoundException()
}
}
}
3.2.2 Frontend usage
If your CIBA status endpoint is /api/auth/oidc/token, then you can use the following code on the frontend:
import { checkCibaStatusClient } from '@ones.dev/common'
const host = 'https://example.com'
const authReqId = '...'
const { access_token, id_token, expires_in, token_type } = await checkCibaStatusClient(host, authReqId)
3.3 Refresh Access Token
// Reuse the device authentication from the previous example
const auth = new DeviceAuthentication({ ... })
const refreshResult = await auth.refreshAccessToken(reqBody.refresh_token)
4.0 QR
Implement QR flow on server side, for OnesID users to authenticate using a QR code.
Available from v0.17.102.
Here’s what this looks like:
- Backend: Generate a new QR session
- Frontend: Show a QR code to the user
- OnesID application: User scans QR code
- Backend: Generate a new CIBA request from callback -> Standard CIBA flow
- This is automated, as long as you use
DeviceAuthentication
- This is automated, as long as you use
4.1 Make QR Request
On your backend, create a QR auth session from frontend request (GET):
// Reuse the device authentication from the previous example
const auth = new DeviceAuthentication({ ... })
// Capture IPv4 from where the request originated
// Example: You want to show a QR code to a user in the browser, and capture the user IP address on your server endpoint
const originOriginIPv4 = req.headers['x-forwarded-for'] || req.socket.remoteAddress
// Create a QR auth session on your server
const {
cbUrl,
sessionId,
expIn,
exp,
interval
} = await auth.makeQrAuthSessionIdp()
return {
cbUrl,
sessionId,
exp,
interval
}
4.1.1 Example
On a NestJS backend, this might look like this:
@Public()
@Post('/api/auth/qr/make')
async makeQrAuthSession(): Promise<QrAuthSessionIdp> {
return auth.makeQrAuthSessionIdp()
}
4.1.2 Frontend usage
If your QR session endpoint is /api/auth/qr/make, then you can use the following code on the frontend:
import { makeQrSessionClientV2 } from '@ones.dev/common'
const host = 'https://example.com'
const { sessionId, cbUrl, expIn } = await makeQrSessionClientV2(host)
4.2 Generate QR code from QR Request
On the frontend, generate a QR code and show it to the user:
// Generate a QR code and show it to the user
const newQrCode: AuthenticationQRCodeContent {
cbUrl: session.cbUrl,
sessionId: session.sessionId,
}
4.3 Poll QR Session Status
On your backend, start polling the QR session status, and wait for the user to approve the request:
// Start polling the session status
const {
sessionId,
exp,
// once authRequestId is set, you can switch to the CIBA flow
authRequestId
} = await auth.verifyQrAuthSessionIdp(sessionId)
Once the authRequestId (auth_req_id) is set, you should return it to your frontend, and switch to the CIBA flow: 3.2 Check CIBA Status.
Key points:
- The QR session exists only, to learn about the encoded user identifier, to start a CIBA request
- The QR session is deleted once the user has approved the request
- As soon as your receive the
authRequestId(set bymakeCibaRequestWithOptionalQrSession), you can switch to the CIBA flow
4.4.1 Example
On a NestJS backend, this might look like this:
@Public()
@Post('/api/auth/qr/status')
async verifyQrAuthSession(
@Body() body: {
sessionId: string
},
): Promise<QrAuthSessionIdp> {
const qrSession = await auth.verifyQrAuthSessionIdp({
sessionId: body.sessionId,
...
})
if (!qrSession) {
throw new NotFoundException('Session not found')
}
return qrSession
}
4.4.2 Frontend usage
If your QR session endpoint is /api/auth/qr/status, then you can use the following code on the frontend:
import { qrSessionStatusClientV2 } from '@ones.dev/common'
const host = 'https://example.com'
const sessionId = '...'
const { authRequestId } = await qrSessionStatusClientV2(
host,
sessionId
)
5.0 Consent API
Request user consent for personal data.
Available from v0.17.88.
5.1 Supported properties
You may request any of the following properties:
import { PERSONAL_DATA_CONSENT_PROPERTIES } from "@ones.dev/common"
export enum PERSONAL_DATA_CONSENT_PROPERTIES {
FIRST_NAME = 'firstName',
LAST_NAME = 'lastName',
LOCALIZED_FIRST_NAME = 'localizedFirstName',
LOCALIZED_LAST_NAME = 'localizedLastName',
IDENTITY_DOCUMENT_NUMBER = 'identityDocumentNumber',
IDENTITY_DOCUMENT_ISSUE_DATE = 'identityDocumentIssueDate',
IDENTITY_DOCUMENT_EXPIRY_DATE = 'identityDocumentExpiryDate',
DATE_OF_BIRTH = 'dateOfBirth',
EMAIL = 'email',
PHONE_NUMBER = 'phoneNumber',
LOCALIZED_FULL_NAME = 'localizedFullName',
FULL_NAME = 'fullName',
}
5.2 Make consent request
Make a consent request to obtain user’s personal data.
// Initialize, or re-use the device authentication
const auth = new DeviceAuthentication({ ... })
// Obtain the subject from the JWT via auth.validateJwt(jwt), sub field
const subject = 'UUID of target user'
const resource = 'https://example.com'
const content = [
{
name: PERSONAL_DATA_CONSENT_PROPERTIES.EMAIL,
},
{
name: PERSONAL_DATA_CONSENT_PROPERTIES.PHONE_NUMBER,
}
]
const reason = 'Need email and phone number'
const request = await auth.makeConsentRequest(
subject,
resource,
content,
reason
)
At this point the user should approve the consent request on OnesID mobile application.
5.3 Poll consent status
Poll the consent status in 3s interval until it is approved, or denied.
// Initialize, or re-use the device authentication
const auth = new DeviceAuthentication({ ... })
const {
status,
content,
} = await auth.checkConsentStatus(request.id)
5.4 Consent Flow
If your application relies on the consent flow, use the following approach:
- Complete CIBA flow (3.1, 3.2)
- Extract the subject from the JWT via
auth.validateJwt(access_token), sub field - Make consent request (5.2) and indicate the properties you need
- Poll the consent status (5.3)
- If approved, requested properties are available in
content - If denied, abort the flow
- If approved, requested properties are available in
Keep in mind, that the consent flow involves an additional step for the user, and should be used only for signup, when the user explicitly requests it, or in rare cases, to update the user’s profile.