@1auth/authn-webauthn
Advanced tools
Comparing version 0.0.0-alpha.32 to 0.0.0-alpha.33
426
index.js
@@ -1,9 +0,11 @@ | ||
import { encrypt, decrypt } from '@1auth/crypto' | ||
import { | ||
options as authnOptions, | ||
getOptions as authnGetOptions, | ||
authenticate as authnAuthenticate, | ||
count as authnCount, | ||
list as authnList, | ||
create as authnCreate, | ||
update as authnUpdate, | ||
verify as authnVerify, | ||
expire as authnExpire | ||
expire as authnExpire, | ||
remove as authnRemove | ||
} from '@1auth/authn' | ||
@@ -20,155 +22,177 @@ import { lookup as accountLookup } from '@1auth/account' | ||
const options = { | ||
id: 'WebAuthn', | ||
origin: undefined, // with https:// | ||
name: undefined, | ||
// minimumAuthenticateAllowCredentials: 3, // Add fake auth ids | ||
secret: { | ||
type: 'secret', | ||
// entropy: 64, // ASVS 2.9.2 | ||
// charPool: characterPoolSize.base64, | ||
otp: false, | ||
expire: null, | ||
// create: async () => randomBase64(webauthnSecret.entropy), // client side | ||
encode: async (data, encryptedKey, sub) => | ||
encrypt(JSON.stringify(data), encryptedKey, sub), | ||
decode: async (encryptedData, encryptedKey, sub) => | ||
jsonParseSecret(await decrypt(encryptedData, encryptedKey, sub)), | ||
verify: async (response, authenticator, rest) => { | ||
console.log('verify', authenticator) | ||
try { | ||
const { verified, authenticationInfo } = | ||
await verifyAuthenticationResponse({ | ||
response, | ||
expectedChallenge: rest.challenge, // TODO not encrypted!! | ||
expectedOrigin: options.origin, | ||
expectedRPID: new URL(options.origin).hostname, | ||
authenticator, | ||
requireUserVerification: true // PassKey | ||
}) | ||
if (!verified) throw new Error('Failed verifyAuthenticationResponse') | ||
authenticator.counter = authenticationInfo.newCounter | ||
return jsonEncodeSecret(authenticator) | ||
} catch (e) { | ||
console.error('webauthn.secret.verify', e) | ||
return false | ||
} | ||
const id = 'WebAuthn' | ||
// minimumAuthenticateAllowCredentials: 3, // Add fake auth ids | ||
const token = { | ||
id, | ||
type: 'token', | ||
// entropy: 64, // ASVS 2.9.2 | ||
// minLength: entropyToCharacterLength(64, charactersAlphaNumeric.length), | ||
otp: true, | ||
expire: 10 * 60, | ||
// create: async () => randomAlphaNumeric(secret.minLength), | ||
encode: (value) => JSON.stringify(value), | ||
decode: (value) => JSON.parse(value), | ||
verify: async (response, value) => { | ||
try { | ||
const { verified, registrationInfo } = await verifyRegistrationResponse({ | ||
...value, | ||
response | ||
}) | ||
if (!verified) throw new Error('Failed verifyRegistrationResponse') | ||
return { registrationInfo: jsonEncodeSecret(registrationInfo) } | ||
} catch (e) { | ||
console.error('@1auth/autn-webauthn token.verify()', e) | ||
return false | ||
} | ||
}, | ||
// create challenge | ||
token: { | ||
type: 'token', | ||
// entropy: 64, // ASVS 2.9.2 | ||
// charPool: characterPoolSize.base64, | ||
otp: true, // Not actually an otp, part of the secret | ||
expire: 10 * 60, | ||
// create: async () => randomChallenge(options.token.entropy), | ||
encode: async (value, encryptedKey, sub) => | ||
encrypt(value, encryptedKey, sub), | ||
decode: async (value, encryptedKey, sub) => | ||
decrypt(value, encryptedKey, sub), | ||
verify: async (response, expectedChallenge) => { | ||
try { | ||
const { verified, registrationInfo } = await verifyRegistrationResponse( | ||
{ | ||
response, | ||
expectedChallenge, | ||
expectedOrigin: options.origin, | ||
expectedRPID: new URL(options.origin).hostname, | ||
requireUserVerification: true // PassKey | ||
} | ||
) | ||
if (!verified) throw new Error('Failed verifyRegistrationResponse') | ||
// registrationInfo.challenge = expectedChallenge | ||
return jsonEncodeSecret(registrationInfo) | ||
} catch (e) { | ||
console.error('webauthn.token.verify', e) | ||
return false | ||
} | ||
} | ||
} | ||
} | ||
export default (params) => { | ||
Object.assign(options, authnOptions, params) | ||
const secret = { | ||
id, | ||
type: 'secret', | ||
// entropy: 64, // ASVS 2.9.2 | ||
// charPool: characterPoolSize.base64, | ||
// minLength: entropyToCharacterLength(64, charactersAlphaNumeric.length), | ||
otp: false, | ||
encode: (value) => { | ||
value = jsonEncodeSecret(value) | ||
value = JSON.stringify(value) | ||
return value | ||
}, | ||
decode: (value) => { | ||
value = JSON.parse(value) | ||
value = jsonParseSecret(value) | ||
return value | ||
} | ||
} | ||
// to be sent to client | ||
export const authenticateOptions = async (sub) => { | ||
const userAuthenticators = await options.store.selectList( | ||
options.table, | ||
{ sub, type: options.id + '-' + options.secret.type } | ||
// [ 'value' ] | ||
) | ||
const allowCredentials = [] | ||
const id = [] | ||
for (const credential of userAuthenticators) { | ||
const value = await options.secret.decode( | ||
credential.value, | ||
credential.encryptionKey, | ||
sub | ||
const challenge = { | ||
id, | ||
type: 'challenge', | ||
// entropy: 64, // ASVS 2.9.2 | ||
// minLength: entropyToCharacterLength(112, charactersAlphaNumeric.length), | ||
otp: true, | ||
expire: 10 * 60, | ||
// create: () => randomAlphaNumeric(challenge.minLength), | ||
encode: (value) => { | ||
value.authenticator = jsonEncodeSecret(value.authenticator) | ||
value = JSON.stringify(value) | ||
return value | ||
}, | ||
decode: (value) => { | ||
value = JSON.parse(value) | ||
value.authenticator = jsonParseSecret(value.authenticator) | ||
return value | ||
}, | ||
verify: async (response, value) => { | ||
try { | ||
const { verified, authenticationInfo } = | ||
await verifyAuthenticationResponse({ | ||
...value, | ||
response | ||
}) | ||
if (!verified) throw new Error('Failed verifyAuthenticationResponse') | ||
value.authenticator.counter = authenticationInfo.newCounter | ||
value.authenticator = jsonEncodeSecret(value.authenticator) | ||
return true | ||
} catch (e) { | ||
console.error('@1auth/autn-webauthn challenge.verify()', e) | ||
return false | ||
} | ||
}, | ||
cleanup: async (sub, value, { sourceId } = {}) => { | ||
const now = nowInSeconds() | ||
const { encryptionKey } = await options.store.select( | ||
options.table, | ||
{ id: sourceId, sub }, | ||
['encryptionKey'] | ||
) | ||
id.push(credential.id) | ||
console.log('authenticateOptions', value) | ||
allowCredentials.push({ | ||
id: value.credentialID, | ||
type: 'public-key' | ||
await authnUpdate(options.secret, sub, { | ||
id: sourceId, | ||
encryptedKey: encryptionKey, | ||
value: value.authenticator, | ||
update: now, | ||
lastused: now | ||
}) | ||
} | ||
/* while ( | ||
allowCredentials.length < options.minimumAuthenticateAllowCredentials | ||
) { | ||
const id = randomAlphaNumeric(256) // 43 char - make hash from username to make static | ||
allowCredentials.push({ | ||
id, | ||
type: 'public-key' | ||
}) | ||
} */ | ||
} | ||
const defaults = { | ||
id, | ||
origin: undefined, // with https:// | ||
name: undefined, | ||
secret, | ||
token, | ||
challenge | ||
} | ||
const options = {} | ||
export default (params) => { | ||
Object.assign(options, authnGetOptions(), defaults, params) | ||
} | ||
export const getOptions = () => options | ||
const clientOptions = await generateAuthenticationOptions({ | ||
rpID: new URL(options.origin).hostname, | ||
allowCredentials, | ||
userVerification: 'preferred' | ||
}) | ||
// TODO find a better way, not efficient or save as it's own OTP | ||
await options.store.update( | ||
options.table, | ||
{ id, sub }, | ||
{ | ||
challenge: clientOptions.challenge, // TODO not encrypted!! | ||
update: nowInSeconds() | ||
} | ||
) | ||
return clientOptions | ||
export const count = async (sub) => { | ||
if (options.log) { | ||
options.log('@1auth/autn-webauthn count(', sub, ')') | ||
} | ||
return await authnCount(options.secret, sub) | ||
} | ||
export const authenticate = async (username, secret) => { | ||
const { sub, id, encryptionKey, ...value } = await authnAuthenticate( | ||
username, | ||
secret, | ||
options | ||
) | ||
await authnUpdate( | ||
options.secret.type, | ||
{ sub, id, encryptionKey, value, lastused: nowInSeconds() }, | ||
options | ||
) | ||
return sub | ||
export const list = async (sub) => { | ||
if (options.log) { | ||
options.log('@1auth/autn-webauthn list(', sub, ')') | ||
} | ||
return await authnList(options.secret, sub) | ||
} | ||
export const createToken = async (sub) => { | ||
// const token = await options.token.create() | ||
export const authenticate = async (username, input) => { | ||
if (options.log) { | ||
options.log('@1auth/autn-webauthn authenticate(', username, input, ')') | ||
} | ||
return await authnAuthenticate(options.challenge, username, input) | ||
} | ||
const userAuthenticators = await options.store.selectList(options.table, { | ||
sub, | ||
type: options.id + '-' + options.secret.type | ||
export const create = async (sub) => { | ||
if (options.log) { | ||
options.log('@1auth/autn-webauthn create(', sub, ')') | ||
} | ||
return await createToken(sub) | ||
} | ||
export const verify = async (sub, response, { name } = {}, notify = true) => { | ||
if (options.log) { | ||
options.log( | ||
'@1auth/autn-webauthn verify(', | ||
sub, | ||
response, | ||
({ name } = {}), | ||
notify, | ||
')' | ||
) | ||
} | ||
const value = await verifyToken(sub, response) | ||
const { id } = await authnCreate(options.secret, sub, { | ||
name, | ||
value, | ||
verify: nowInSeconds() | ||
}) | ||
if (notify) { | ||
await options.notify.trigger('authn-webauthn-create', sub) // TODO add in user.name | ||
} | ||
return { id, secret: value } | ||
} | ||
const createToken = async (sub) => { | ||
if (options.log) { | ||
options.log('@1auth/autn-webauthn createToken(', sub, ')') | ||
} | ||
const [credentials, account] = await Promise.all([ | ||
authnList(options.secret, sub, undefined, ['encryptionKey', 'value']), | ||
accountLookup(sub) | ||
]) | ||
const excludeCredentials = [] | ||
for (const credential of userAuthenticators) { | ||
const value = await options.secret.decode( | ||
credential.value, | ||
credential.encryptionKey, | ||
sub | ||
) | ||
console.log('createToken', value) | ||
for (let i = credentials.length; i--;) { | ||
const credential = credentials[i] | ||
const value = options.secret.decode(credential.value) | ||
excludeCredentials.push({ | ||
@@ -180,10 +204,7 @@ id: value.credentialID, | ||
let { username } = await accountLookup(sub) | ||
username ??= 'username' | ||
const clientOptions = await generateRegistrationOptions({ | ||
const registrationOptions = { | ||
rpName: options.name, | ||
rpID: new URL(options.origin).hostname, | ||
userID: isoUint8Array.fromUTF8String(sub), | ||
userName: username, | ||
userName: account.username ?? 'username', | ||
attestationType: 'none', | ||
@@ -210,41 +231,105 @@ excludeCredentials, | ||
// ] | ||
} | ||
if (options.log) { | ||
options.log('@1auth/autn-webauthn createToken', { registrationOptions }) | ||
} | ||
const secret = await generateRegistrationOptions(registrationOptions) | ||
const { id } = await authnCreate(options.token, sub, { | ||
value: { | ||
expectedChallenge: secret.challenge, | ||
expectedOrigin: options.origin, | ||
expectedRPID: new URL(options.origin).hostname, | ||
requireUserVerification: true // PassKey | ||
} | ||
}) | ||
await authnCreate( | ||
options.token.type, | ||
{ sub, value: clientOptions.challenge }, | ||
options | ||
) | ||
return clientOptions // needs to be sent to the client | ||
if (options.log) { | ||
options.log( | ||
'@1auth/autn-webauthn createToken return', | ||
JSON.stringify(secret, null, 2) | ||
) | ||
} | ||
return { id, secret } | ||
} | ||
export const verifyToken = async (sub, credential) => { | ||
const { id, ...value } = await authnVerify( | ||
options.token.type, | ||
const verifyToken = async (sub, credential) => { | ||
if (options.log) { | ||
options.log('@1auth/autn-webauthn verifyToken(', sub, credential, ')') | ||
} | ||
const { registrationInfo } = await authnVerify( | ||
options.token, | ||
sub, | ||
credential, | ||
options | ||
credential | ||
) | ||
delete value.sub | ||
await authnExpire(sub, id, options) | ||
return value | ||
return registrationInfo | ||
} | ||
export const create = async (sub, name, value, onboard = false) => { | ||
await authnCreate( | ||
options.secret.type, | ||
{ sub, name, value, verify: nowInSeconds() }, | ||
options | ||
) | ||
export const createChallenge = async (sub) => { | ||
if (options.log) { | ||
options.log('@1auth/autn-webauthn createChallenge(', sub, ')') | ||
} | ||
// const challenge = options.challenge.create(); | ||
const now = nowInSeconds() | ||
if (!onboard) { | ||
await options.notify.trigger('authn-webauthn-create', sub) // TODO add in user.name | ||
const credentials = await authnList(options.secret, sub, undefined, [ | ||
'id', | ||
'encryptionKey', | ||
'value' | ||
]) | ||
const allowCredentials = [] | ||
for (let i = credentials.length; i--;) { | ||
const credential = credentials[i] | ||
const authenticator = options.secret.decode(credential.value) | ||
allowCredentials.push({ | ||
id: authenticator.credentialID, | ||
type: 'public-key' | ||
}) | ||
} | ||
const authenticationOptions = { | ||
rpID: new URL(options.origin).hostname, | ||
allowCredentials, | ||
userVerification: 'preferred' | ||
} | ||
const secret = await generateAuthenticationOptions(authenticationOptions) | ||
const challenges = [] | ||
for (let i = credentials.length; i--;) { | ||
const credential = credentials[i] | ||
const authenticator = options.secret.decode(credential.value) | ||
challenges.push( | ||
authnCreate(options.challenge, sub, { | ||
sourceId: credential.id, | ||
value: { | ||
authenticator, | ||
expectedChallenge: secret.challenge, | ||
expectedOrigin: options.origin, | ||
expectedRPID: new URL(options.origin).hostname, | ||
requireUserVerification: true // PassKey | ||
}, | ||
update: now | ||
}) | ||
) | ||
} | ||
const id = await Promise.all(challenges) | ||
if (options.log) { | ||
options.log('@1auth/autn-webauthn createChallenge', { secret }, '') | ||
} | ||
return { id, secret } | ||
} | ||
export const list = async (sub, type = options.id + '-secret') => { | ||
return options.store.selectList(options.table, { sub, type }) | ||
export const expire = async (sub, id) => { | ||
if (options.log) { | ||
options.log('@1auth/autn-webauthn remove(', sub, id, ')') | ||
} | ||
await authnExpire(options.secret, sub, id) | ||
await options.notify.trigger('authn-webauthn-expire', sub) | ||
} | ||
export const remove = async (sub, id) => { | ||
await authnExpire(sub, id, options) | ||
if (options.log) { | ||
options.log('@1auth/autn-webauthn remove(', sub, id, ')') | ||
} | ||
await authnRemove(options.secret, sub, id) | ||
await options.notify.trigger('authn-webauthn-remove', sub) | ||
@@ -261,5 +346,2 @@ } | ||
const jsonParseSecret = (value) => { | ||
if (typeof value !== 'string') value = JSON.stringify(value) | ||
value = JSON.parse(value) | ||
// value.credentialID = credentialBuffer(value.credentialID); | ||
@@ -266,0 +348,0 @@ value.credentialPublicKey = credentialBuffer(value.credentialPublicKey) |
{ | ||
"name": "@1auth/authn-webauthn", | ||
"version": "0.0.0-alpha.32", | ||
"version": "0.0.0-alpha.33", | ||
"description": "", | ||
"type": "module", | ||
"engines": { | ||
"node": ">=16" | ||
"node": ">=20" | ||
}, | ||
@@ -47,3 +47,3 @@ "engineStrict": true, | ||
"homepage": "https://github.com/willfarrell/1auth", | ||
"gitHead": "3750bef3d7e376c48f7d680e5f2181ee809213b9", | ||
"gitHead": "14b8c5bd83728c460fdcc4c3af5ae5c3c2bb9007", | ||
"dependencies": { | ||
@@ -50,0 +50,0 @@ "@simplewebauthn/server": "10.0.0" |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
12028
3
334
1