Skip to content

Commit

Permalink
Implement the loginWithWebAuthn method (#99)
Browse files Browse the repository at this point in the history
* Implement login with webauthn method

* Fix route

* Fix tslint errors
  • Loading branch information
roxanemace authored Jun 15, 2020
1 parent 1e4c489 commit f777676
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 3 deletions.
52 changes: 49 additions & 3 deletions src/main/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import { UrlParser } from './urlParser'
import { popupSize } from './providerPopupSize'
import { createHttpClient, HttpClient } from './httpClient'
import { computePkceParams, PkceParams } from './pkceService'
import { encodePublicKeyCredentialCreationOptions, serializeRegistrationPublicKeyCredential, publicKeyCredentialType, CredentialCreationOptionsSerialized } from './webAuthnService'
import {
encodePublicKeyCredentialCreationOptions, encodePublicKeyCredentialRequestOptions,
serializeRegistrationPublicKeyCredential, serializeAuthenticationPublicKeyCredential,
CredentialCreationOptionsSerialized, CredentialRequestOptionsSerialized,
publicKeyCredentialType
} from './webAuthnService'

export type SignupParams = {
data: SignupProfile
Expand All @@ -38,6 +43,10 @@ export type LoginWithCredentialsParams = {
auth?: AuthOptions
}

type EmailLoginWithWebAuthnParams = { email: string, auth?: AuthOptions }
type PhoneNumberLoginWithWebAuthnParams = { phoneNumber: string, auth?: AuthOptions }
export type LoginWithWebAuthnParams = EmailLoginWithWebAuthnParams | PhoneNumberLoginWithWebAuthnParams

type EmailRequestPasswordResetParams = {
email: string
redirectUrl?: string
Expand Down Expand Up @@ -96,6 +105,8 @@ export type TokenRequestParameters = {
persistent?: boolean // Whether the remember me is enabled
}

type InternalToken = { tkn: string }

/**
* Identity Rest API Client
*/
Expand Down Expand Up @@ -382,7 +393,7 @@ export default class ApiClient {

private loginWithPasswordByRedirect({ auth = {}, ...rest }: LoginWithPasswordParams): Promise<void> {
return this.http
.post<{ tkn: string }>('/password/login', {
.post<InternalToken>('/password/login', {
body: {
clientId: this.config.clientId,
scope: this.resolveScope(auth),
Expand Down Expand Up @@ -454,7 +465,7 @@ export default class ApiClient {
})
.then(result => this.eventManager.fireEvent('authenticated', result))
: this.http
.post<{ tkn: string }>('/signup', {
.post<InternalToken>('/signup', {
body: {
clientId: this.config.clientId,
redirectUrl,
Expand Down Expand Up @@ -632,6 +643,41 @@ export default class ApiClient {
})
}

loginWithWebAuthn(params: LoginWithWebAuthnParams): Promise<void> {
const body = {
clientId: this.config.clientId,
origin: window.location.origin,
scope: this.resolveScope(params.auth),
email: (params as EmailLoginWithWebAuthnParams).email,
phoneNumber: (params as PhoneNumberLoginWithWebAuthnParams).phoneNumber
}

return this.http
.post<CredentialRequestOptionsSerialized>('/webauthn/authentication-options', { body })
.then(response => {
const options = encodePublicKeyCredentialRequestOptions(response.publicKey)

return navigator.credentials.get({ publicKey: options })
})
.then(credentials => {
if (!credentials || credentials.type !== publicKeyCredentialType) {
throw new Error('Unable to authenticate with invalid public key crendentials.')
}

const serializedCredentials = serializeAuthenticationPublicKeyCredential(credentials)

return this.http
.post<InternalToken>('/webauthn/authentication', { body: { ...serializedCredentials } })
.then(response => this.loginWithPasswordCallback(response.tkn, params.auth))
.catch(error => { throw error })
})
.catch(error => {
if (error.error) this.eventManager.fireEvent('login_failed', error)

throw error
})
}

getSessionInfo(): Promise<SessionInfo> {
return this.http.get<SessionInfo>('/sso/data', {
query: { clientId: this.config.clientId },
Expand Down
7 changes: 7 additions & 0 deletions src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Profile, RemoteSettings, SessionInfo } from './models'
import ApiClient, {
LoginWithPasswordParams,
LoginWithCredentialsParams,
LoginWithWebAuthnParams,
PasswordlessParams,
VerifyPasswordlessParams,
RequestPasswordResetParams,
Expand Down Expand Up @@ -56,6 +57,7 @@ export type Client = {
loginWithCustomToken: (params: { token: string; auth: AuthOptions }) => Promise<void>
loginWithCredentials: (params: LoginWithCredentialsParams) => Promise<void>
addNewWebAuthnDevice: (accessToken: string) => Promise<void>
loginWithWebAuthn: (params: LoginWithWebAuthnParams) => Promise<void>
getSessionInfo: (params?: {}) => Promise<SessionInfo>
checkUrlFragment: (url: string) => boolean
}
Expand Down Expand Up @@ -184,6 +186,10 @@ export function createClient(creationConfig: Config): Client {
return apiClient.then(api => api.addNewWebAuthnDevice(accessToken))
}

function loginWithWebAuthn(params: LoginWithWebAuthnParams) {
return apiClient.then(api => api.loginWithWebAuthn(params))
}

function getSessionInfo() {
return apiClient.then(api => api.getSessionInfo())
}
Expand Down Expand Up @@ -237,6 +243,7 @@ export function createClient(creationConfig: Config): Client {
loginWithCustomToken,
loginWithCredentials,
addNewWebAuthnDevice,
loginWithWebAuthn,
getSessionInfo,
checkUrlFragment
}
Expand Down
52 changes: 52 additions & 0 deletions src/main/webAuthnService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { encodeToBase64 } from '../utils/base64'
export const publicKeyCredentialType = 'public-key'

export type CredentialCreationOptionsSerialized = { publicKey: PublicKeyCredentialCreationOptionsSerialized }
export type CredentialRequestOptionsSerialized = { publicKey: PublicKeyCredentialRequestOptionsSerialized }

type PublicKeyCredentialCreationOptionsSerialized = {
rp: PublicKeyCredentialRpEntity
Expand All @@ -22,6 +23,18 @@ type PublicKeyCredentialCreationOptionsSerialized = {
extensions?: AuthenticationExtensionsClientInputs
}

type PublicKeyCredentialRequestOptionsSerialized = {
challenge: string
timeout?: number
rpId: string
allowCredentials: {
id: string
transports?: AuthenticatorTransport[]
type: PublicKeyCredentialType
}[]
userVerification: 'required' | 'preferred' | 'discouraged'
}

export type RegistrationPublicKeyCredentialSerialized = {
id: string
rawId: string
Expand All @@ -32,6 +45,18 @@ export type RegistrationPublicKeyCredentialSerialized = {
}
}

export type AuthenticationPublicKeyCredentialSerialized = {
id: string
rawId: string
type: 'public-key'
response: {
authenticatorData: string
clientDataJSON: string
signature: string
userHandle: string | null
}
}

export function encodePublicKeyCredentialCreationOptions(serializedOptions: PublicKeyCredentialCreationOptionsSerialized): PublicKeyCredentialCreationOptions {
return {
...serializedOptions,
Expand All @@ -43,6 +68,17 @@ export function encodePublicKeyCredentialCreationOptions(serializedOptions: Publ
}
}

export function encodePublicKeyCredentialRequestOptions(serializedOptions: PublicKeyCredentialRequestOptionsSerialized): PublicKeyCredentialRequestOptions {
return {
...serializedOptions,
challenge: Buffer.from(serializedOptions.challenge, 'base64'),
allowCredentials: serializedOptions.allowCredentials.map(allowCrendential => ({
...allowCrendential,
id: Buffer.from(allowCrendential.id, 'base64')
}))
}
}

export function serializeRegistrationPublicKeyCredential(encodedPublicKey: PublicKeyCredential): RegistrationPublicKeyCredentialSerialized {
const response = encodedPublicKey.response as AuthenticatorAttestationResponse

Expand All @@ -56,3 +92,19 @@ export function serializeRegistrationPublicKeyCredential(encodedPublicKey: Publi
}
}
}

export function serializeAuthenticationPublicKeyCredential(encodedPublicKey: PublicKeyCredential): AuthenticationPublicKeyCredentialSerialized {
const response = encodedPublicKey.response as AuthenticatorAssertionResponse

return {
id: encodedPublicKey.id,
rawId: encodeToBase64(encodedPublicKey.rawId),
type: encodedPublicKey.type,
response: {
authenticatorData: encodeToBase64(response.authenticatorData),
clientDataJSON: encodeToBase64(response.clientDataJSON),
signature: encodeToBase64(response.signature),
userHandle: response.userHandle && encodeToBase64(response.userHandle)
}
}
}

0 comments on commit f777676

Please sign in to comment.