AWS日記24 (Cognito - OIDC)

# はじめに
今回は AWS Cognito の OpenID Connect (OIDC) を試します。
OIDCを利用しGitHubとアカウント連携します。
[GitHub OpenID Connect Wrapper for Cognito](https://github.com/TimothyJones/github-cognito-openid-wrapper)を参考に進めます。

[Lambda関数・SAMテンプレート](https://github.com/tanaka-takurou/serverless-account-page-go/tree/github)

# 準備
[AWS SAM の準備をします](https://qiita.com/tanaka_takurou/items/cae7c19fc3aaf0031abb)

[AWS Cognitoの資料]
[AWS Cognito](https://aws.amazon.com/jp/cognito/)
[ユーザープールへの OIDC ID プロバイダーの追加](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-oidc-idp.html)

# AWS SAM テンプレート作成

AWS SAM テンプレートで API-Gateway , Lambda, AWS Cognito の設定をします。

[参考資料]
[AWS SAM テンプレートを作成する](https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/tutorial-lambda-sam-template.html)

## AWS Cognito設定用のSAMテンプレート

### AWS Cognito の設定は以下の部分
```yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Serverless Account Page

Parameters:
  ApplicationName:
    Type: String
    Default: 'ServerlessAccountPage'
  UserPoolClientName:
    Type: String
    Default: 'GitHubOpenIdAuthClient'
  UserPoolDefaultDomainPrefixName:
    Type: String
    Default: 'test-user-pool-domain2021'
  UserPoolClientCallbackURL:
    Type: String
    Default: ''

Resources:
  UserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: false
        UnusedAccountValidityDays: 7
      AutoVerifiedAttributes:
        - 'email'
      MfaConfiguration: 'OFF'
  UserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Properties:
      UserPoolId: !Ref UserPool
      ClientName: !Ref UserPoolClientName
      AllowedOAuthFlows:
        - 'implicit'
      AllowedOAuthFlowsUserPoolClient: true
      AllowedOAuthScopes:
        - 'openid'
      ExplicitAuthFlows:
        - 'ALLOW_REFRESH_TOKEN_AUTH'
      GenerateSecret: false
      CallbackURLs:
        - !Ref UserPoolClientCallbackURL
  UserPoolDomain:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      UserPoolId: !Ref UserPool
      Domain: !Ref UserPoolDefaultDomainPrefixName

Outputs:
  APIURI:
    Description: "Amazon Cognito Domain"
    Value: !Join [ '', [ 'https://', !Ref UserPoolDomain, '.auth.', !Ref 'AWS::Region', '.amazoncognito.com'] ]

```

## GitHub OAuth App ラッパー用のSAMテンプレート
```yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Serverless Cognito GitHub OpenId Page

Globals:
  Function:
    Runtime: go1.x
    Timeout: 15
    Environment:
      Variables:
        REGION: !Ref 'AWS::Region'
        ALGORITHM: !Ref Algorithm
        KEY_ID: !Ref KeyId
        CERT_PATH: !Ref CertPath
        PUB_KEY_PATH: !Ref PubKeyPath
        GITHUB_CLIENT_ID: !Ref GitHubClientId
        GITHUB_CLIENT_SECRET: !Ref GitHubClientSecret
        COGNITO_REDIRECT_URI: !Ref CognitoRedirectUri
        GITHUB_API_URL: !Ref GitHubUrl
        GITHUB_LOGIN_URL: !Ref GitHubLoginUrl

Parameters:
  GitHubClientId:
    Type: String
  GitHubClientSecret:
    Type: String
  CognitoRedirectUri:
    Type: String
  GitHubUrl:
    Type: String
    Default: "https://api.github.com"
    MinLength: 1
  GitHubLoginUrl:
    Type: String
    Default: "https://github.com/login"
    MinLength: 1
  StageName:
    Type: String
    Default: "CognitoGitHubApiStage"
  CertPath:
    Type: String
    Default: "jwtRS256.key"
  PubKeyPath:
    Type: String
    Default: "jwtRS256.key.pub"
  Algorithm:
    Type: String
    Default: "RS256"
  KeyId:
    Type: String
    Default: "jwtRS256"

Resources:
  GithubOAuthApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref StageName
      OpenApiVersion: "2.0"
  OpenIdDiscovery:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: api/discovery/bin/
      Handler: main
      MemorySize: 256
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /.well-known/openid-configuration
            Method: get
            RestApiId: !Ref GithubOAuthApi
  Authorize:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: api/authorize/bin/
      Handler: main
      MemorySize: 256
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /authorize
            Method: get
            RestApiId: !Ref GithubOAuthApi
  Token:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: api/token/bin/
      Handler: main
      MemorySize: 256
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /token
            Method: get
            RestApiId: !Ref GithubOAuthApi
        PostResource:
          Type: Api
          Properties:
            Path: /token
            Method: post
            RestApiId: !Ref GithubOAuthApi
  UserInfo:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: api/userinfo/bin/
      Handler: main
      MemorySize: 256
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /userinfo
            Method: get
            RestApiId: !Ref GithubOAuthApi
        PostResource:
          Type: Api
          Properties:
            Path: /userinfo
            Method: post
            RestApiId: !Ref GithubOAuthApi
  Jwks:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: api/jwks/bin/
      Handler: main
      MemorySize: 256
      Events:
        GetResource:
          Type: Api
          Properties:
            Path: /.well-known/jwks.json
            Method: get
            RestApiId: !Ref GithubOAuthApi

Outputs:
    CognitoGitHub:
      Description: "Cognito GitHub Api URL"
      Value: !Sub "https://${GithubOAuthApi}.execute-api.${AWS::Region}.amazonaws.com/${StageName}"

```

## フロントエンド用のSAMテンプレート
```yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Serverless Cognito OpenId FrontPage

Parameters:
  ApplicationName:
    Type: String
    Default: 'ServerlessCognitoOpenIdFrontPage'
  CognitoUrl:
    Type: String
    Default: 'https://your-domain-prefix.auth.your-region.amazoncognito.com'
  CognitoClientId:
    Type: String
    Default: 'abc012'

Resources:
  FrontPageApi:
    Type: AWS::Serverless::HttpApi
  FrontPageFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: ServerlessCognitoOpenIdFrontFunction
      CodeUri: bin/
      Handler: main
      MemorySize: 256
      Runtime: go1.x
      Description: 'Cognito OpenId Front Function'
      Environment:
        Variables:
          REGION: !Ref 'AWS::Region'
          COGNITO_URL: !Ref CognitoUrl
          COGNITO_CLIENT_ID: !Ref CognitoClientId
      Events:
        FrontPageApi:
          Type: HttpApi
          Properties:
            Path: '/'
            Method: get
            ApiId: !Ref FrontPageApi
        FrontProxyApi:
          Type: HttpApi
          Properties:
            Path: '/{proxy+}'
            Method: get
            ApiId: !Ref FrontPageApi

Outputs:
  APIURI:
    Description: "URI"
    Value: !Join [ '', [ 'https://', !Ref FrontPageApi, '.execute-api.',!Ref 'AWS::Region','.amazonaws.com/'] ]

```

# Lambda関数作成

※ JWT周りの処理は github.com/dgrijalva/jwt-go を利用しました。

```golang
func getJwks()(JSONWebKey, error) {
	res := JSONWebKey{}
	res.Algorithm = os.Getenv("ALGORITHM")
	res.KeyId = os.Getenv("KEY_ID")
	res.KeyType = "RSA"
	res.PublicKeyUse = "sig"
	key, err := os.ReadFile(os.Getenv("PUB_KEY_PATH"))
	if err != nil {
		return res, err
	}
	verifyKey, err := jwt.ParseRSAPublicKeyFromPEM(key)
	if err != nil {
		return res, err
	}
	res.RSAModulus = joseBase64UrlEncode(verifyKey.N.Bytes())
	res.RSAExponent = joseBase64UrlEncode(serializeRSAPublicExponentParam(verifyKey.E))
	return res, nil
}

func joseBase64UrlEncode(b []byte) string {
	return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
}

func serializeRSAPublicExponentParam(e int) []byte {
	buf := make([]byte, 4)
	binary.BigEndian.PutUint32(buf, uint32(e))
	var i int
	for i = 0; i < 8; i++ {
		if buf[i] != 0 {
			break
		}
	}
	return buf[i:]
}
```

# 終わりに
外部サービス(GitHub)の設定を行う必要があるため、SAMテンプレートを複数に分ける必要がありました。
GitHubだけでなく他の外部サービスにも応用ができるため、今後試そうと思います。
今回JWTに触れることになり、とても勉強になりました。

<details><summary>参考資料</summary><div>

[GitHub OpenID Connect Wrapper for Cognito](https://github.com/TimothyJones/github-cognito-openid-wrapper)


[AWS Cognitoのエンドポイントを使いこなす](https://qiita.com/poruruba/items/a284b9f522d8d6e1e3f9)

[なんちゃってOAuth2/OpenID Connectサーバを自作する](https://qiita.com/poruruba/items/042adba2ac9a34839546)

[AWS CognitoとなんちゃってOpenID ConnectサーバをIDフェデレーションする](https://qiita.com/poruruba/items/6ea55dd2befb92df6628)

[Amazon Cognito JSON ウェブトークンの署名をデコードして検証するにはどうすればよいですか?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/decode-verify-cognito-json-token/)

[RFC 7519 - JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519)

[JSON ウェブトークンの検証](https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html)

[RSA公開鍵のJWKをPEM形式にする](https://www.ykrods.net/posts/2019/05/02/make-rsa-public-pem/)

[IDトークンが分かれば OpenID Connect が分かる](https://qiita.com/TakahikoKawasaki/items/8f0e422c7edd2d220e06)

[Go言語で理解するJWT認証 実装ハンズオン](https://qiita.com/po3rin/items/740445d21487dfcb5d9f)

[jwt-goを用いた、GolangでのJWTの発行、認証について](https://qiita.com/wh1teB0x/items/e2133eeb94f57629b5e7)

[GoでJWTまわりTips](https://qiita.com/maruware/items/df376a1a1742aade4177)