Authentication Flow: NestJS Module

This guide provides detailed instructions for using the Authentication Flow on a server with a NestJS backend, and Vue frontend.

Table of content

For NestJS, we have two modules to ease CIBA and QR Code authentication.

  • DeviceAuthModule
  • FrontendAuthModule

This library is powered by our Javascript libraries @ones.dev/common and @ones.dev/backend-modules about which you can read more here.

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/backend-device-auth
npm install @ones.dev/backend-frontend-auth

Important: To use the module you need to register your device with IDP first, and make sure that px-device-identity-service is running. See here.

1.1 Server integration

Here’s what a typical app.module.ts file might look like:

@Module({
    imports: [
        HttpModule,
        ConfigModule.forRoot({
            isGlobal: true,
            load: [() => configuration()],
        }),
        RedisModule.forRootAsync(RedisModule, {
            imports: [ConfigModule],
            useFactory: (configService: ConfigService) => {
                return {
                    config: {
                        host: configService.get('backendConfig.redis.host'),
                        port: configService.get('backendConfig.redis.port'),
                    },
                }
            },
            inject: [ConfigService],
        }),
        // Device authentication
        DeviceAuthModule.forRootAsync(DeviceAuthModule, {
            imports: [HttpModule],
            inject: [ConfigService, RedisService, HttpService],
            useFactory: (
                configService: ConfigService,
                redisService: RedisService,
                httpService: HttpService
            ) => {
                return {
                    redisService,
                    httpService,
                    settings: {
                        applicationName: configService.get('commonConfig.applicationName'),
                        deviceId: configService.get('systemConfig.deviceId'),
                        clientId: configService.get('systemConfig.clientId'),
                        authHost: configService.get('systemConfig.authServerUrl'),
                        host: configService.get('backendConfig.host'),
                        deviceIdentityUrl: configService.get('backendConfig.oidc.deviceIdentityUrl'),
                    },
                }
            },
        }),
        // Frontend authentication
        FrontendAuthModule.forRootAsync(FrontendAuthModule, {
            imports: [ConfigModule, HttpModule],
            inject: [ConfigService, RedisService, DeviceAuthService, HttpService],
            useFactory: (
                configService: ConfigService,
                redisService: RedisService,
                deviceAuthService: DeviceAuthService,
                httpService: HttpService,
            ) => {
                return {
                    redisService,
                    deviceAuthService,
                    deviceAuthConfig: {
                        applicationName: configService.get('commonConfig.applicationName'),
                        deviceId: configService.get('systemConfig.deviceId'),
                        clientId: configService.get('systemConfig.clientId'),
                        authHost: configService.get('systemConfig.authServerUrl'),
                        host: configService.get('backendConfig.host'),
                        deviceIdentityUrl: configService.get('backendConfig.oidc.deviceIdentityUrl'),
                    },
                    httpService,
                    config: {
                        applicationName: configService.get('commonConfig.applicationName'),
                        authHost: configService.get('systemConfig.authServerUrl'),
                        host: configService.get('backendConfig.host'),
                    },
                }
            },
        }),
    ],
    controllers: [],
    providers: [],
})
export class AppModule {}

Here’s an excerpt of the tests, to illustrate the usage:

const repoRoot = findRepoRootSync()

describe('Device Auth', () => {
  let app: INestApplication;
  let mobileDeviceAuth: MobileDeviceAuthentication
  let masterAccessToken: string
  let application: ApplicationEntity

  let mobileCliConfig: CLIConfig

  beforeAll(async () => {
    const config = await loadSystemConfig()
    const appConfig = configuration()
    mobileCliConfig = await getCliConfig(`${repoRoot}/`)
    mobileDeviceAuth = new MobileDeviceAuthentication({
        name: 'Mobile device',
        idpBaseUrl: config.authServerUrl,
        deviceClientId: mobileCliConfig.properties['mobile.client_id'],
        authJwtProperties: new AuthJwtProperties(),
        deviceIdentityUrl: appConfig.backendConfig.oidc.deviceIdentityUrl,
    })
    const keyPath = `${repoRoot}/.cli/.cli.mobile.pem`
    const keys = await loadKeys(keyPath)
    mobileDeviceAuth.loadKeys(keys)

    const newApplicationDomain = `domain-${Date.now()}.com`
    masterAccessToken = await runMasterCibaAuth(repoRoot, appConfig.backendConfig.oidc.deviceIdentityUrl)

    const moduleRef = await Test.createTestingModule({
      imports: [
        ConfigModule.forRoot({
            isGlobal: true,
            load: [() => configuration()],
        }),
        HttpModule,
        RedisModule.forRootAsync(RedisModule, {
            imports: [ConfigModule],
            inject: [ConfigService],
            useFactory: (configService: ConfigService) => {
                return {
                    config: {
                      host: configService.get('backendConfig.redis.host'),
                      port: configService.get('backendConfig.redis.port'),
                    },
                    global: true,
                }
            },
        }),
        DeviceAuthModule.forRootAsync(DeviceAuthModule, {
            imports: [HttpModule, ConfigModule],
            inject: [HttpService, ConfigService],
            useFactory: (httpService: HttpService, configService: ConfigService) => ({
                httpService,
                settings: {
                    applicationName: 'testing',
                    deviceId: config.deviceId,
                    clientId: config.clientId,
                    authHost: config.authServerUrl,
                    host: `http://${newApplicationDomain}`,
                    deviceIdentityUrl: configService.get('backendConfig.oidc.deviceIdentityUrl'),
                },
                global: true,
            }),
        }),
        FrontendAuthModule.forRootAsync(FrontendAuthModule, {
            imports: [ConfigModule, HttpModule],
            inject: [ConfigService, HttpService],
            useFactory: (
                httpService: HttpService,
            ) => {
                return {
                    httpService,
                    config: {
                        applicationName: 'testing',
                        authHost: config.authServerUrl,
                        host: `http://${newApplicationDomain}`,
                    },
                }
            },
        }),
      ],
    })
      .compile();

    app = moduleRef.createNestApplication();
    await app.init();

    // Register application on IDP
    const newApplication = await cmApplications.create(axiosConfigDefault(masterAccessToken), {
        name: 'Some application',
        type: APPLICATION_TYPE.SERVER,
        metadata: {
            hostname: newApplicationDomain,
            isDependent: true,
            clientIdentifier: 'some-app',
        },
        device: {
            id: config.deviceId,
            clientId: config.clientId,
        } as DeviceEntity,
        isEnabled: true,
    })
    application = newApplication.data
      
  });

  it(`Get device JWT`, async () => {
    const token = await app.get(DeviceAuthService).makeDeviceJwt();
    expect(token).toBeDefined();
  });

  it(`Get device access token`, async () => {
    const token = await app.get(DeviceAuthService).makeDeviceAccessToken();
    expect(token).toBeDefined();
  })

  /**
   * NEW QR Login
   */
  let qrSessionId: string
  it('Make QR session', async () => {
    const session = await app.get(FrontendAuthService).makeQrAuthSession(
      '10.10.10.100'
    )
    expect(session).toBeDefined()
    expect(session.cbUrl.endsWith('/auth/oidc/bc-authorize')).toBeTruthy()
    qrSessionId = session.sessionId
  })

  it('Check QR session', async () => {
    const session = await app.get(FrontendAuthService).verifyQrAuthSession(qrSessionId)
    expect(session).toBeDefined()
    expect(session.cbUrl.endsWith('/auth/oidc/bc-authorize')).toBeTruthy()
  })

  it('[LEGACY] Make QR session http', async () => {
    const response = await request(app.getHttpServer())
      .get('/auth/qr')
      .expect(200)

    expect(response.body).toBeDefined()
    expect(response.body.deviceId).toBeDefined()
    expect(response.body.auth_req_id).toBeDefined()
    expect(response.body.exp).toBeDefined()
  })

  let authRequestIdGlob: string
  it('Make CIBA request from QR session ID', async () => {
    const loginHintToken = await mobileDeviceAuth.getLoginHintToken()
    const response = await request(app.getHttpServer())
      .post('/auth/oidc/bc-authorize')
      .send({
        loginHintKind: loginHintToken.kind,
        userIdentifier: loginHintToken.value,
        sessionId: qrSessionId,
        resource: CIBA_AUTH_REQUEST_RESOURCE.IDP,
        scope: 'openid offline_access profile',
      })
      
    expect(response.body.auth_req_id).toBeDefined()
    authRequestIdGlob = response.body.auth_req_id
  })

  it('Check CIBA status: PENDING', async () => {
    expect(authRequestIdGlob).toBeDefined()
    const statusRequest: CibaAuthStatusRequestFrontendDto = {
      grant_type: 'urn:openid:params:grant-type:ciba',
      authRequestId: authRequestIdGlob,
    }
    const response = await request(app.getHttpServer())
      .post(`/auth/oidc/token`)
      .send(statusRequest)

    expect(response.status).toBe(400)
    expect(response.body.error).toBe('authorization_pending')
  })

  it('[user mobile] Approve request', async () => {
    const pendingCiba = await mobileDeviceAuth.listPendingCibaRequests()
    expect(pendingCiba).toBeDefined()
    const cibaRequest = pendingCiba.find((ciba) => ciba.authRequestId === authRequestIdGlob)
    expect(cibaRequest).toBeDefined()
    await mobileDeviceAuth.respondToPendingCibaRequest(cibaRequest.authRequestId, 'approved')
  })

  let refresh_token: string
  it('Check CIBA status: SUCCESS', async () => {
    const statusRequest: CibaAuthStatusRequestFrontendDto = {
      grant_type: 'urn:openid:params:grant-type:ciba',
      authRequestId: authRequestIdGlob,
    }
    const response = await request(app.getHttpServer())
      .post(`/auth/oidc/token`)
      .send(statusRequest)

    expect(response.status).toBe(201)
    expect(response.body.access_token).toBeDefined()
    expect(response.body.refresh_token).toBeDefined()
    expect(response.body.scope).toBe('openid offline_access profile')
    
    refresh_token = response.body.refresh_token
  })

  it('Refresh token', async () => {
    const refreshRequest: RefreshTokenRequestFrontendDto = {
      grant_type: 'refresh_token',
      refresh_token: refresh_token,
    }

    const response = await request(app.getHttpServer())
      .post(`/auth/oidc/token`)
      .send(refreshRequest)

    expect(response.status).toBe(201)
    expect(response.body.access_token).toBeDefined()
    expect(response.body.refresh_token).toBeDefined()
    expect(response.body.scope).toBe('openid offline_access profile')
  })

  it('Make CIBA request with resource', async () => {
    const loginHintToken = await mobileDeviceAuth.getLoginHintToken()
    const response = await request(app.getHttpServer())
      .post('/auth/oidc/bc-authorize')
      .send({
        loginHintKind: loginHintToken.kind,
        userIdentifier: loginHintToken.value,
        sessionId: qrSessionId,
        resource: CIBA_AUTH_REQUEST_RESOURCE.DEVICE,
        scope: 'openid offline_access profile api:read api:write',
      })
      
    expect(response.body.auth_req_id).toBeDefined()
    const authRequestId = response.body.auth_req_id

    await mobileDeviceAuth.respondToPendingCibaRequest(authRequestId, 'approved')

    const statusRequest: CibaAuthStatusRequestFrontendDto = {
      grant_type: 'urn:openid:params:grant-type:ciba',
      authRequestId,
    }

    const tokenResponse = await request(app.getHttpServer())
      .post(`/auth/oidc/token`)
      .send(statusRequest)

    expect(tokenResponse.status).toBe(201)
    expect(isJWT(tokenResponse.body.access_token)).toBeTruthy()
    expect(tokenResponse.body.scope).toBe('')

    const verificationResult = await app.get(FrontendAuthService).validateJwt(tokenResponse.body.access_token)
    expect(verificationResult.jti).toBeDefined()
  })

  it('Make CIBA request from phone, with QR session', async () => {
    const session = await app.get(FrontendAuthService).makeQrAuthSession(
      '10.10.10.100'
    )

    // Respond from phone
    const loginHintToken = await mobileDeviceAuth.getLoginHintToken()
    const ciba = await request(app.getHttpServer())
      .post('/auth/oidc/bc-authorize')
      .send({
        loginHintKind: loginHintToken.kind,
        userIdentifier: loginHintToken.value,
        sessionId: session.sessionId,
        resource: CIBA_AUTH_REQUEST_RESOURCE.DEVICE,
        scope: 'openid offline_access profile api:read api:write',
      })

      // Approve
      try {
        await mobileDeviceAuth.respondToPendingCibaRequest(
          ciba.body.auth_req_id, 'approved'
        )
      } catch (err) {
        console.error((err as AxiosError).response.data)
      }

      // Check status from client
      const status = await app.get(FrontendAuthService).verifyQrAuthSession(session.sessionId)
      expect(status.authRequestId).toBe(ciba.body.auth_req_id)

      // Check ciba status from client
      const cibaStatus = await app.get(FrontendAuthService).cibaStatus(ciba.body.auth_req_id)
      expect(cibaStatus.access_token).toBeDefined()
  })

  it('Make CIBA request with username and resource', async () => {
    const response = await request(app.getHttpServer())
      .post('/auth/oidc/bc-authorize')
      .send({
        loginHintKind: LOGIN_HINT_KIND.LOGIN_HINT,
        userIdentifier: mobileCliConfig.properties['mobile.username'],
        resource: CIBA_AUTH_REQUEST_RESOURCE.DEVICE,
        scope: 'openid offline_access profile api:read api:write',
      })
      
    expect(response.body.auth_req_id).toBeDefined()
    const authRequestId = response.body.auth_req_id

    await mobileDeviceAuth.respondToPendingCibaRequest(authRequestId, 'approved')

    const statusRequest: CibaAuthStatusRequestFrontendDto = {
      grant_type: 'urn:openid:params:grant-type:ciba',
      authRequestId,
    }

    const tokenResponse = await request(app.getHttpServer())
      .post(`/auth/oidc/token`)
      .send(statusRequest)

    expect(tokenResponse.status).toBe(201)
    expect(isJWT(tokenResponse.body.access_token)).toBeTruthy()
    expect(tokenResponse.body.scope).toBe('')

    const verificationResult = await app.get(FrontendAuthService).validateJwt(tokenResponse.body.access_token)
    expect(verificationResult.jti).toBeDefined()
  })

  afterAll(async () => {
    await app.close();
  });
});

1.2 Frontend integration

Coming soon…

Contact

Found a problem, or have a question related to Authentication Flow: NestJS Module?

ONES Now Documentation

© 2025 ONES Now Documentation | Author Franz Geffke