Add controllers and packages for security keys

This commit is contained in:
Matthew Penner 2022-10-24 09:44:16 -06:00
parent f8ec8b4d5a
commit 06f692e649
No known key found for this signature in database
29 changed files with 2398 additions and 383 deletions

View file

@ -0,0 +1,85 @@
import useSWR, { ConfigInterface } from 'swr';
import { useStoreState } from '@/state/hooks';
import http, { FractalResponseList } from '@/api/http';
import { SecurityKey, Transformers } from '@definitions/user';
import { AxiosError } from 'axios';
import { decodeBase64 } from '@/lib/base64';
import { decodeBuffer, encodeBuffer } from '@/lib/buffer';
import { LoginResponse } from '@/api/auth/login';
import { useUserSWRKey } from '@/plugins/useSWRKey';
function decodeSecurityKeyCredentials(credentials: PublicKeyCredentialDescriptor[]) {
return credentials.map(c => ({
id: decodeBuffer(decodeBase64(c.id.toString())),
type: c.type,
transports: c.transports,
}));
}
function useSecurityKeys(config?: ConfigInterface<SecurityKey[], AxiosError>) {
const uuid = useStoreState(state => state.user.data!.uuid);
const key = useUserSWRKey(['account', 'security-keys']);
return useSWR<SecurityKey[], AxiosError>(
key,
async (): Promise<SecurityKey[]> => {
const { data } = await http.get('/api/client/account/security-keys');
return (data as FractalResponseList).data.map((datum) => Transformers.toSecurityKey(datum.attributes));
},
{ revalidateOnMount: false, ...(config || {}) },
);
}
async function deleteSecurityKey(uuid: string): Promise<void> {
await http.delete(`/api/client/account/security-keys/${uuid}`);
}
async function registerCredentialForAccount(name: string, tokenId: string, credential: PublicKeyCredential): Promise<SecurityKey> {
const { data } = await http.post('/api/client/account/security-keys/register', {
name,
token_id: tokenId,
registration: {
id: credential.id,
type: credential.type,
rawId: encodeBuffer(credential.rawId),
response: {
attestationObject: encodeBuffer((credential.response as AuthenticatorAttestationResponse).attestationObject),
clientDataJSON: encodeBuffer(credential.response.clientDataJSON),
},
},
});
return Transformers.toSecurityKey(data.attributes);
}
async function registerSecurityKey(name: string): Promise<SecurityKey> {
const { data } = await http.get('/api/client/account/security-keys/register');
const publicKey = data.data.credentials;
publicKey.challenge = decodeBuffer(decodeBase64(publicKey.challenge));
publicKey.user.id = decodeBuffer(publicKey.user.id);
if (publicKey.excludeCredentials) {
publicKey.excludeCredentials = decodeSecurityKeyCredentials(publicKey.excludeCredentials);
}
const credentials = await navigator.credentials.create({ publicKey });
if (!credentials || credentials.type !== 'public-key') {
throw new Error(`Unexpected type returned by navigator.credentials.create(): expected "public-key", got "${credentials?.type}"`);
}
return await registerCredentialForAccount(name, data.data.token_id, credentials as PublicKeyCredential);
}
// eslint-disable-next-line camelcase
async function authenticateSecurityKey(data: { confirmation_token: string; data: string }): Promise<LoginResponse> {
const response = await http.post('/auth/login/checkpoint/key', data);
return {
complete: response.data.complete,
intended: response.data.data?.intended || null,
};
}
export { useSecurityKeys, deleteSecurityKey, registerSecurityKey, authenticateSecurityKey };

View file

@ -1,24 +1,6 @@
import { Model, UUID } from '@/api/definitions';
import { SubuserPermission } from '@/state/server/subusers';
interface User extends Model {
uuid: string;
username: string;
email: string;
image: string;
twoFactorEnabled: boolean;
createdAt: Date;
permissions: SubuserPermission[];
can(permission: SubuserPermission): boolean;
}
interface SSHKey extends Model {
name: string;
publicKey: string;
fingerprint: string;
createdAt: Date;
}
interface ActivityLog extends Model<'actor'> {
id: string;
batch: UUID | null;
@ -33,3 +15,30 @@ interface ActivityLog extends Model<'actor'> {
actor: User | null;
};
}
interface User extends Model {
uuid: string;
username: string;
email: string;
image: string;
twoFactorEnabled: boolean;
createdAt: Date;
permissions: SubuserPermission[];
can(permission: SubuserPermission): boolean;
}
interface SecurityKey extends Model {
uuid: UUID;
name: string;
type: 'public-key';
publicKeyId: string;
createdAt: Date;
updatedAt: Date;
}
interface SSHKey extends Model {
name: string;
publicKey: string;
fingerprint: string;
createdAt: Date;
}

View file

@ -3,6 +3,36 @@ import { FractalResponseData } from '@/api/http';
import { transform } from '@definitions/helpers';
export default class Transformers {
static toActivityLog = ({ attributes }: FractalResponseData): Models.ActivityLog => {
const { actor } = attributes.relationships || {};
return {
id: attributes.id,
batch: attributes.batch,
event: attributes.event,
ip: attributes.ip,
isApi: attributes.is_api,
description: attributes.description,
properties: attributes.properties,
hasAdditionalMetadata: attributes.has_additional_metadata ?? false,
timestamp: new Date(attributes.timestamp),
relationships: {
actor: transform(actor as FractalResponseData, this.toUser, null),
},
};
};
static toSecurityKey (data: Record<string, any>): Models.SecurityKey {
return {
uuid: data.uuid,
name: data.name,
type: data.type,
publicKeyId: data.public_key_id,
createdAt: new Date(data.created_at),
updatedAt: new Date(data.updated_at),
};
}
static toSSHKey = (data: Record<any, any>): Models.SSHKey => {
return {
name: data.name,
@ -26,25 +56,6 @@ export default class Transformers {
},
};
};
static toActivityLog = ({ attributes }: FractalResponseData): Models.ActivityLog => {
const { actor } = attributes.relationships || {};
return {
id: attributes.id,
batch: attributes.batch,
event: attributes.event,
ip: attributes.ip,
isApi: attributes.is_api,
description: attributes.description,
properties: attributes.properties,
hasAdditionalMetadata: attributes.has_additional_metadata ?? false,
timestamp: new Date(attributes.timestamp),
relationships: {
actor: transform(actor as FractalResponseData, this.toUser, null),
},
};
};
}
export class MetaTransformers {}