Cloudflareのworkersのitty-router-openapiでauthorizationヘッダで認証する

経緯

  1. itty-router-openapiのendpointのschemaのauthorizationヘッダを追加した。
  2. OpenAPIページのI/Fに表示されているが、OpenAPIページからAPIを実行しても送信されていない。
  3. curlコマンドでauthorizationヘッダありでAPIを実行すると、API側では期待通りの動作をする。

OpenAPIページからauthorizationヘッダが送信されていない。
authorizationヘッダは、endpointのschemaに定義するは間違い。

itty-router-openapiでの認証の方法

security componentを使用する
Security

全てのエンドポイントで認証を必要をする場合の設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export const router = OpenAPIRouter({
  docs_url: "/",
  schema: {
    security: [
      {
        BearerAuth: [],
      },
    ],
  },
});

router.registry.registerComponent(
  'securitySchemes',
  'BearerAuth',
  {
    type: 'http',
    scheme: 'bearer',
  },
)

// 認証処理を行う
router.all('/*', Authenticate)
// 認証が必要なエンドポイントはAuthenticateの後
router.get('something-endpoint', SomethingEndpooint)

Authorizeの鍵マークが表示されるようになり、OpenAPIページからauthorizationヘッダが送信されるようになった。

認証処理の例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import * as jose from "jose";
import {JWTVerifyOptions} from "jose/dist/types/jwt/verify";
import {z} from 'zod'

const userSchema = z.object({
  sub: z.string(),
  name: z.string(),
  nickname: z.string(),
  picture: z.string(),
  email: z.string(),
  email_verified: z.boolean(),
  updated_at: z.string(),
});

const getBearer = (request: Request): null | string => {
  const authHeader = request.headers.get('authorization')
  if (!authHeader || authHeader.substring(0, 6) !== 'Bearer') {
    return null
  }
  return authHeader.substring(6).trim()
}

const verify = async (token: string, env: any) => {
  const JWKS = await jose.createRemoteJWKSet(new URL(env.JWKS_URL))
  const options:JWTVerifyOptions = { 
    issuer: env.JWT_ISSUER,
      audience: env.JWT_AUDIENCE,
    }
  const {payload, protectedHeader} =
    await jose.jwtVerify(token, JWKS, options).catch(async (error) => {
      if (error?.code === 'ERR_JWKS_MULTIPLE_MATCHING_KEYS') {
        for await (const publicKey of error) {
          try {
            return await jose.jwtVerify(token, publicKey, options)
          } catch (innerError) {
            if (innerError?.code === 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED') {
              continue
            }
            throw innerError
          }
        }
        throw new jose.errors.JWSSignatureVerificationFailed()
      }
      throw error
    })
  return {payload, protectedHeader}
}
const getUserInfo = async (token: string, env: any,) => {
  const payload = jose.decodeJwt(token)
  let userInfoEndPoint:string
  if (Array.isArray(payload.aud)) {
    userInfoEndPoint = payload.aud.find(e => e.endsWith("userinfo"))
  } else {
    if (payload.aud.endsWith("userinfo")) {
      userInfoEndPoint = payload.aud
    }
  }
  if (userInfoEndPoint) {
  const res = await fetch(userInfoEndPoint, {
    method: 'GET',
    headers: {
      'Authorization': 'Bearer ' + token,
    }
  })
  if (res.ok) {
      const validatedUser = userSchema.safeParse(await res.json())
      if (!validatedUser.success) {
          throw new Error("Unable to retrieve user information");
      }
      if (!validatedUser.data.email_verified) {
          throw new Error("User Email not verified");
      }
      return validatedUser.data
    }
  }
}

export async function Authenticate(request: Request, env: any, context: any) {
  const token = getBearer(request)
  if (!token) {
    throw new Error("token not found");
  }
  try {
    const {payload, protectedHeader} = await verify(token, env)
  } catch(error) {
    if (!(error instanceof CustomHttpStatus)) {
      throw new Error("token not verified");
    }
  }
  const user = await getUserInfo(token, env)
  env.user = user
  return
}

Last Mod: May 19, 2024