@e280/authlocal
Version:
User-sovereign login system for everybody
57 lines (44 loc) • 1.74 kB
text/typescript
import {decodeToken} from "./decode.js"
import {tokentime} from "./tokentime.js"
import {verify} from "../crypto/crypto.js"
import {Token, TokenVerifications, TokenVerifyError} from "./types.js"
export async function verifyToken<P extends Token>(
id: string,
token: string,
options: TokenVerifications = {},
): Promise<P> {
const [headerText, payloadText] = token.split(".")
const {payload, signature} = decodeToken<P>(token)
const signingText = `${headerText}.${payloadText}`
const signingBytes = new TextEncoder().encode(signingText)
if (!await verify(signingBytes, signature, id))
throw new TokenVerifyError("token signature invalid")
if (options.atTime !== null) {
const atTime = options.atTime ?? Date.now()
if (payload.exp) {
const expiresAt = tokentime.toMs(payload.exp)
if (atTime > expiresAt)
throw new TokenVerifyError("token expired")
}
if (payload.nbf) {
const notBefore = tokentime.toMs(payload.nbf)
if (atTime < notBefore)
throw new TokenVerifyError("token not ready")
}
}
if (options.allowedIssuers) {
if (!payload.iss)
throw new TokenVerifyError(`required iss (issuer) is missing`)
if (!options.allowedIssuers.includes(payload.iss))
throw new TokenVerifyError(`invalid iss (issuer) "${payload.iss}"`)
}
if (options.allowedAudiences) {
if (!payload.aud)
throw new TokenVerifyError(`required aud (audience) is missing`)
if (!options.allowedAudiences.includes(payload.aud))
throw new TokenVerifyError(`invalid aud (audience) "${payload.aud}"`)
}
if (payload.aud && !options.allowedAudiences)
throw new TokenVerifyError(`allowedAudiences verification option was not provided, but is required because the token included "aud"`)
return payload
}