bearerAuth function

Middleware bearerAuth({
  1. Object? token,
  2. FutureOr<bool> verifyToken(
    1. String token,
    2. Context c
    )?,
  3. String prefix = 'Bearer',
  4. String headerName = 'authorization',
  5. String? hashFunction(
    1. String input
    )?,
  6. BearerAuthErrorOptions? noAuthenticationHeader,
  7. BearerAuthErrorOptions? invalidAuthenticationHeader,
  8. BearerAuthErrorOptions? invalidToken,
})

Bearer token authentication middleware.

Validates the Authorization: Bearer <token> header (or a custom header / prefix). Supply either token (one static token or a list) or a verifyToken callback — not both.

Three distinct error cases:

  • No Authorization header → 401 Unauthorized
  • Malformed header (bad format/chars) → 400 Bad Request
  • Valid format but token rejected → 401 Unauthorized

Each case is independently customisable via noAuthenticationHeader, invalidAuthenticationHeader, and invalidToken.

// Static token
app.mount('/api/*', bearerAuth(token: 'my-secret'));

// Multiple valid tokens
app.mount('/api/*', bearerAuth(token: ['token-a', 'token-b']));

// Custom verification
app.mount('/api/*', bearerAuth(
  verifyToken: (token, c) => token == env.secret,
));

// Custom prefix / header
app.mount('/api/*', bearerAuth(
  token: 'secret',
  prefix: 'Token',
  headerName: 'x-api-key',
));

// Custom error responses
app.mount('/api/*', bearerAuth(
  token: 'secret',
  noAuthenticationHeader: BearerAuthErrorOptions(
    message: (c) => {'error': 'No token provided'},
  ),
  invalidAuthenticationHeader: BearerAuthErrorOptions(
    message: (c) => {'error': 'Malformed token'},
  ),
  invalidToken: BearerAuthErrorOptions(
    message: (c) => {'error': 'Token rejected'},
  ),
));

Implementation

Middleware bearerAuth({
  Object? token, // String | List<String>
  FutureOr<bool> Function(String token, Context c)? verifyToken,
  String prefix = 'Bearer',
  String headerName = 'authorization',
  String? Function(String input)? hashFunction,
  BearerAuthErrorOptions? noAuthenticationHeader,
  BearerAuthErrorOptions? invalidAuthenticationHeader,
  BearerAuthErrorOptions? invalidToken,
}) {
  assert(
    token != null || verifyToken != null,
    'bearerAuth requires either "token" or "verifyToken"',
  );

  final wwwPrefix = prefix.isEmpty ? '' : '$prefix ';

  return (Context c, Next next) async {
    final rawHeader = c.req.header(headerName);

    // ── 1. No Authorization header ────────────────────────────────────────────
    if (rawHeader == null) {
      await _respond(
        c,
        status: 401,
        wwwAuthenticate:
            noAuthenticationHeader?.wwwAuthenticate ?? '${wwwPrefix}realm=""',
        messageOption: noAuthenticationHeader?.message,
        defaultMessage: 'Unauthorized',
      );
      return;
    }

    // ── 2. Malformed / invalid header format ──────────────────────────────────
    String? tokenValue;

    if (prefix.isEmpty) {
      tokenValue = rawHeader;
    } else if (rawHeader.toLowerCase().startsWith(prefix.toLowerCase()) &&
        rawHeader.length > prefix.length &&
        rawHeader[prefix.length] == ' ') {
      tokenValue = rawHeader.substring(prefix.length + 1).trimLeft();
    }

    final validChars = RegExp(r'^[A-Za-z0-9._~+/\-]+=*$');

    if (tokenValue == null ||
        tokenValue.isEmpty ||
        !validChars.hasMatch(tokenValue)) {
      await _respond(
        c,
        status: 400,
        wwwAuthenticate: invalidAuthenticationHeader?.wwwAuthenticate ??
            '${wwwPrefix}error="invalid_request"',
        messageOption: invalidAuthenticationHeader?.message,
        defaultMessage: 'Bad Request',
      );
      return;
    }

    // ── 3. Validate token ─────────────────────────────────────────────────────
    bool valid = false;

    if (verifyToken != null) {
      valid = await verifyToken(tokenValue, c);
    } else if (token is String) {
      valid = await _timingSafeEqual(token, tokenValue, hashFunction);
    } else if (token is List) {
      for (final t in token.cast<String>()) {
        if (await _timingSafeEqual(t, tokenValue, hashFunction)) {
          valid = true;
          break;
        }
      }
    }

    if (!valid) {
      await _respond(
        c,
        status: 401,
        wwwAuthenticate: invalidToken?.wwwAuthenticate ??
            '${wwwPrefix}error="invalid_token"',
        messageOption: invalidToken?.message,
        defaultMessage: 'Unauthorized',
      );
      return;
    }

    await next();
  };
}