OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-identity / lib / auth / providers / bearer-token.js

123 lines · javascript ref:
import { BaseProvider } from '../base-provider.js'
import crypto from 'hypercore-crypto'
import b4a from 'b4a'

/**
 * BearerTokenProvider — API token authentication
 *
 * Manages bearer tokens with embedded permissions.
 * Tokens are stored as SHA256 hashes for security.
 */
export class BearerTokenProvider extends BaseProvider {
  constructor (identityRegistry, opts = {}) {
    super('bearer-token')
    this.registry = identityRegistry
    // Use separate db for tokens (not Autobase view)
    this.db = opts.db || (identityRegistry && identityRegistry.db) || null
  }

  async extractCredentials (req) {
    const authHeader = req.headers.authorization
    if (!authHeader || !authHeader.startsWith('Bearer ')) return null
    return { token: authHeader.substring(7) }
  }

  async authenticate (credentials) {
    const { token } = credentials
    const tokenData = await this.validateToken(token)
    if (!tokenData) throw new Error('Invalid token')

    return {
      identityId: `token:${tokenData.hash.substring(0, 16)}`,
      method: 'bearer-token',
      credentials: { username: tokenData.username, tokenHash: tokenData.hash },
      permissions: tokenData.permissions || ['read'],
      resources: tokenData.resources || ['*']
    }
  }

  /**
   * Create a new bearer token
   */
  async createToken (opts = {}) {
    const token = b4a.toString(crypto.randomBytes(32), 'hex')
    const hash = this._hashToken(token)

    const tokenData = {
      hash,
      username: opts.username || 'anonymous',
      description: opts.description || '',
      permissions: opts.permissions || ['read'],
      resources: opts.resources || ['*'],
      createdAt: Date.now(),
      expiresAt: opts.expiresAt || null
    }

    if (this.db) {
      await this.db.put(`token:${hash}`, tokenData)
    }

    return { token, ...tokenData }
  }

  /**
   * Validate a token
   */
  async validateToken (token) {
    if (!this.db) return null
    const hash = this._hashToken(token)
    const entry = await this.db.get(`token:${hash}`)
    if (!entry) return null

    const data = entry.value
    if (data.expiresAt && data.expiresAt < Date.now()) return null
    if (data.revoked) return null

    return data
  }

  /**
   * Revoke a token
   */
  async revokeToken (tokenHash) {
    if (!this.db) return
    const entry = await this.db.get(`token:${tokenHash}`)
    if (entry) {
      entry.value.revoked = true
      entry.value.revokedAt = Date.now()
      await this.db.put(`token:${tokenHash}`, entry.value)
    }
  }

  /**
   * List all tokens
   */
  async listAllTokens () {
    if (!this.db) return []
    const tokens = []
    for await (const entry of this.db.createReadStream({
      gte: 'token:',
      lt: 'token:\xff'
    })) {
      tokens.push(entry.value)
    }
    return tokens
  }

  /**
   * Get tokens for specific user
   */
  async getUserTokens (username) {
    const all = await this.listAllTokens()
    return all.filter(t => t.username === username)
  }

  _hashToken (token) {
    // Use hypercore-crypto for hashing (works in both Node and Bare)
    const data = b4a.from(token)
    return b4a.toString(crypto.data(data), 'hex')
  }
}

export default BearerTokenProvider