Skip to content

Instantly share code, notes, and snippets.

@tMinamiii
Last active October 17, 2023 11:22
Show Gist options
  • Save tMinamiii/9e8ac44f3667781c83aa5da05d7ccfd0 to your computer and use it in GitHub Desktop.
Save tMinamiii/9e8ac44f3667781c83aa5da05d7ccfd0 to your computer and use it in GitHub Desktop.
oidc

grpc link id token

-> Authfront End ->

dynamo table and relations

  • user_auth(1) : user_attribute(*)
  • user_authとuser_attributeは、uid で紐づく

user auth table

  • uid とは、auth serverが生成する、1ユーザーに紐づく共通UniqueID
  • sid とは、アプリごとのID。 ヘルスケアでは hc:1111111-22222222-333333333-44444444

(e.g)

  • uid aaaaaaaa-bbbbbbbb-cccccccc-ddddddd sid hc:1111111-22222222-333333333-44444444 で1レコード
  • uid aaaaaaaa-bbbbbbbb-cccccccc-ddddddd sid dk:1111111-22222222-333333333-44444444 で1レコード

user attribute table

user attributeのUK key

  • PKは、uid aaaaaaaa-bbbbbbbb-cccccccc-ddddddd
    • hc_id: uuid
    • google_id_token_sub: sub
    • line_id_token_sub: sub
    • apple_id_token_sub: sub
    • apple_refresh_token: refresh token
      • Appleだけは、apple.RefreshToken も保存しておかないといけない
      • Appleの規約で退会時にRevokeしないといけない

initiate auth

以下の2つにになる

  • email / pass (graphql)
  • sub / IDTokenまるごと (sns)

sub / IDTokenまるごと

/callback endpoint でやること

hc_id が user_attribute に存在しない場合

  • token exchange
  • 新規ユーザーユーザー作成 AdminCreateUser
    • sub / IDTokenまるごと (sns)
      • lambda + Custom flow
    • UserAttribute(key: line.idtoken.sub, val: subClaim)
  • hc_idを発行
  • token発行

hc_id が user_attribute に存在する場合

sub が user_attribute に存在しない

  • UserAttribute(key: line.IDToken val: subClaim)
  • InitialteAuthでtoken発行

sub が user_attribute に存在する

  • InitialteAuthでtoken発行

  • user_attributeとuser_authができおり
    • /token
    • code
    • user_idとidToken

どちらでのendpointでexchangeするかで、引数と返却値がかわる

  • /tokenの場合 code <-> id_tokenとuser_id

  • /callback
    code <-> accessToken, idToken, refreshToken

結論

codeは外部に出したくないので /callback で exchange する

OAuth2.0について

ブラウザでapi-serverにアクセス LINEのログインページにRedirect

LINEアプリにログインしていれば、 ログイン後、LINEの認可画面にRedirect

LINE 認証フロー

クライアント
-> idp url
-> リダイレクト
-> line oidc endpoint
-> line app
-> 認証
-> line webview
-> callback url
-> idp post auth url
-> サーバサイドバックグラウンド
-> line token exange
-> link every id & line id
-> generate auth token(for exange every id token)
-> redirect url with auth token
-> クライアント

注釈版

クライアントから見るとcb a(ディープリンク)を指定したらcb aにauth tokenがついて帰ってくるのでシンプルなんですが、中身は複雑ですね

lineのopenid configuration https://access.line.me/.well-known/openid-configuration

クライアント
-> idp url(cb A)  [e.g.] open: https://auth-api.every.tv/line/login?callback_url=healthcare://login
-> リダイレクト   [e.g.] Code 302, Location: https://access.line.me/oauth2/v2.1/authorize?redirect_uri=idp_enddpoint
-> line oidc endpoint(cb B)  [e.g.] open: https://access.line.me/oauth2/v2.1/authorize?redirect_uri=idp_enddpoint
-> line app [e.g.] スマホでアドレスを開くとline appが起動
-> 認証     [e.g.] line appで認証許可

-- このあたりからうまくイメージができていない.. --
-> line webview  
-> idp post auth url (cb B callback) 
-> IDP サーバサイドバックグラウンド
  -> line token exchange
  -> link every id & line id
  -> generate auth token (for exchange every id token)
  -> redirect url(cb A) with auth token  [e.g.] healthcare://login?auth_token=xxxx
-> クライアント

LoginURL

https://access.line.me/oauth2/v2.1/login
    ?returnUri=/oauth2/v2.1/authorize/consent?scope=openid+profile+friends+groups+timeline.post+message.write
    &response_type=code
    &redirect_uri=https%3A%2F%2Fsocial-plugins.line.me%2Fwidget%2FloginCallback%3FreturnUrl%3Dhttps%253A%252F%252Fsocial-plugins.line.me%252Fwidget%252Fclose
    &state=9fec98665820574ebc349f47d089a6
    &client_id=1446101138
    &loginChannelId=1446101138#/
https://social-plugins.line.me/widget/loginCallback
    ?returnUrl=https://social-plugins.line.me/widget/close

LINE

認可URLにリダイレクトされたユーザーは、以下のいずれかの認証方法でログインできます。

認証方法 説明 自動ログイン ユーザーの操作なしでログイン。LINEログイン画面や確認画面は表示されません メールアドレスログイン LINEログイン画面にメールアドレスとパスワードを入力してログイン QRコードログイン LINEログイン画面に表示されたQRコードを、スマートフォン版LINEのQRコードリーダーでスキャンしてログイン シングルサインオン(SSO)によるログイン 「次のアカウントでログイン」と表示された確認画面でログインボタンをクリックしてログイン

UserIDについて

ProviderごとにUIDを振られるのでDK, TM, HCでUIDを突合できる

リダイレクトURLについて

  • 管理画面のリダイレクトURLに設定したURLしかクエリパラメタのredirect_uriに指定できない
  • 管理画面では複数のリダイレクトURLを設定できる
package handler
import (
"auth-api/form"
"auth-api/service"
"net/http"
"github.com/labstack/echo/v4"
)
type OAuth2 interface {
Token(echo.Context) error
UserInfo(echo.Context) error
Authorize(echo.Context) error
Callback(echo.Context) error
}
type OAuth2Handler struct {
}
func NewOAuth2Handler() OAuth2 {
return &OAuth2Handler{}
}
func (o *OAuth2Handler) Authorize(c echo.Context) error {
f, err := form.NewOAuthAuthorize(c)
if err != nil {
return err
}
s := service.NewOAuth2(c)
ctx := c.Request().Context()
url, err := s.Authorize(ctx, f)
if err != nil {
return err
}
return c.Redirect(http.StatusFound, url)
}
func (o *OAuth2Handler) Callback(c echo.Context) error {
f, err := form.NewOAuthAuthorizeCallback(c)
if err != nil {
return err
}
s := service.NewOAuth2(c)
ctx := c.Request().Context()
appURI, err := s.Callback(ctx, f)
if err != nil {
return err
}
return c.Redirect(http.StatusFound, appURI)
}
package service
import (
"auth-api/form"
"auth-api/infra/httpsess"
"auth-api/model"
"auth-api/openid"
"auth-api/repository"
"context"
"fmt"
"net/http"
"net/url"
"github.com/labstack/echo/v4"
"golang.org/x/oauth2"
)
type OAuth2 interface {
GenerateState() (string, error)
Authorize(ctx context.Context, f *form.OAuthAuthorize) (string, error)
Callback(ctx context.Context, f *form.OAuthAuthorizeCallback) (string, error)
}
type OAuth2Service struct {
authRepo repository.OAuth2Authorize
random RandGenerator
}
func NewOAuth2(c echo.Context) OAuth2 {
return &OAuth2Service{
authRepo: httpsess.NewOAuth2Authorize(c),
random: NewRandGenerator(),
}
}
func (o *OAuth2Service) GenerateState() (string, error) {
return o.random.State(32)
}
func (o *OAuth2Service) Authorize(ctx context.Context, f *form.OAuthAuthorize) (string, error) {
p, ok := openid.GetProvider(f.ProviderName.IssuerURI())
if !ok {
return "", fmt.Errorf("provider not found: %s", f.ProviderName)
}
conf := &oauth2.Config{
ClientID: p.ClientID(),
ClientSecret: p.ClientSecret(),
Scopes: p.Scopes(),
Endpoint: oauth2.Endpoint{
TokenURL: p.TokenEndpoint,
AuthURL: p.AuthorizationEndpoint,
},
RedirectURL: openid.AuthServerCallbackEndpoint(),
}
state, err := o.GenerateState()
if err != nil {
return "", err
}
url := conf.AuthCodeURL(state)
m := model.OAuth2Authorize{
State: state,
RedirectURI: f.RedirectURI,
}
o.authRepo.Save(ctx, m)
return url, nil
}
func (o *OAuth2Service) Callback(ctx context.Context, f *form.OAuthAuthorizeCallback) (string, error) {
m, err := o.authRepo.Load(ctx)
if err != nil {
return "", err
}
if f.State != m.State {
return "", echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("mismatch state"))
}
// TODO: callbackでExchangeまでやるか? いったんCodeを返してExchangeは別のAPIでやるようにするか?
v := make(url.Values, 1)
v.Add("code", f.Code)
return fmt.Sprintf("%s?%s", m.RedirectURI, v.Encode()), nil
}

https://qiita.com/TakahikoKawasaki/items/8f0e422c7edd2d220e06

aud Audience クレーム

RFC 7519

https://datatracker.ietf.org/doc/html/rfc7519

The "aud" (audience) claim identifies the recipients that the JWT is intended for. 
Each principal intended to process the JWT MUST identify itself with a value in the audience claim. 
If the principal processing the claim does not identify itself with a value in the "aud" claim when this claim is present, 
then the JWT MUST be rejected. In the general case, the "aud" value is an array of case-sensitive strings, each containing a StringOrURI value. In the special case when the JWT has one audience, the "aud" value MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience values is generally application specific. Use of this claim is OPTIONAL.

「aud」(オーディエンス)クレームは、JWTが意図する受信者を識別します。
JWTを処理することを意図された各主体は、オーディエンスクレーム内の値で自身を識別する必要があります。
このクレームが存在するときに「aud」クレーム内の値で自身を識別しない主体がクレームを処理する場合、JWTは拒否されなければなりません。
一般的に、「aud」の値は大文字と小文字を区別する文字列の配列で、それぞれがStringOrURI値を含みます。
JWTが1つのオーディエンスを持つ特殊なケースでは、「aud」の値はStringOrURI値を含む単一の大文字と小文字を区別する文字列であってもよいです。
オーディエンス値の解釈は通常、アプリケーション固有です。このクレームの使用は任意です。

OIDC Core

https://openid.net/specs/openid-connect-core-1_0.html

REQUIRED. Audience(s) that this ID Token is intended for. 
It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value. 
It MAY also contain identifiers for other audiences. 
In the general case, the aud value is an array of case sensitive strings. 
In the common special case when there is one audience, the aud value MAY be a single case sensitive string.

必須です。このIDトークンの対象となるオーディエンスです。
オーディエンス値として、依存関係者のOAuth 2.0 client_idを含める必要があります。
他のオーディエンスの識別子も含めることができます。
一般的には、aud値は大文字と小文字を区別する文字列の配列です。
一つのオーディエンスがあるという共通の特例の場合、aud値は大文字と小文字を区別する単一の文字列であってもよいです。

要約

aud クレームは、当該 JWT が、誰を対象として発行されたのかを示すものです。 別の言い方をすると、JWT の受け取り手が誰であるべきかを示しています。 ID トークンの場合、この aud クレームの値は、ID トークンの発行を依頼したクライアントアプリケーションのクライアント ID となります。

OAuth徹底入門より client id (aud) の割当て

まずは重要なことを最初に行いましょう。OAuth では、

クライアントと認可サーバーがお互いにやり 取りを行うのですが、その前にお互いについての情報をいくつか共有しなくてはなりません。 OAuth プロトコル自体はこの情報共有をどのように行うのかについての取り決めはなく、何らかの方法でその情報共有が行われるということだけを知っていれば良いようになっています。 また、OAuth クライアントは クライアント識別子として使われる特別な文字列を使うことで自身を一意に識別されるようにしています。 本書のサンプルや OAuth プロトコルのいくつかの箇所ではこのクライアント識別子を client_id とい う名で参照しています。

また、このクライアント識別子となる文字列は対象の認可サーバーにてクライアントごとに一意になる必要があり、 そのため、通常は認可サーバーによってクライアントに割り振られるようになっています。

この割り振りは開発者用のポータル・サイトを使う方法や動的なクライアント登録(第 12 章で説明します)、 もしくは、何らかのほかの方法によって行われます。

3.1.3.7. ID Token Validation

Client は Token Response 内の ID Token を確認しなければならない (MUST).

  • ID Token が 暗号化されているならば, Client が Registration にて指定し OP が ID Token の暗号化に利用した鍵とアルゴリズムを用いて復号する. Registration 時に OP と暗号化が取り決められても ID Token が暗号化されていなかったときは, RP はそれを拒絶するべき (SHOULD).
  • (一般的に Discovery を通して取得される) OpenID Provider の Issuer Identifier は iss (issuer) Claim の値と正確に一致しなければならない (MUST).
  • Client は aud (audience) Claim が iss (issuer) Claim で示される Issuer にて登録された, 自身の client_id をオーディエンスとして含むことを確認しなければならない (MUST). aud (audience) Claim は複数要素の配列を含んでも良い (MAY). ID Token が Client を有効なオーディエンスとして記載しない, もしくは Client から信用されていない追加のオーディエンスを含むならば, そのID Token は拒絶されなければならない.
  • ID Token が複数のオーディエンスを含むならば, Client は azp Claim があることを確認すべき (SHOULD).
  • azp (authorized party) Claim があるならば, Client は Claim の値が自身の client_id であることを確認すべき (SHOULD). (このフローの中で) ID Token を Client と Token Endpoint の間の直接通信により受け取ったならば, トークンの署名確認の代わりに TLS Server の確認を issuer の確認のために利用してもよい (MAY). Client は JWS [JWS] に従い, JWT alg Header Parameter を用いて全ての ID Token の署名を確認しなければならない (MUST). Client は Issuer から提供された鍵を利用しなければならない (MUST).
  • alg の値はデフォルトの RS256 もしくは Registration にて Client により id_token_signed_response_alg パラメータとして送られたアルゴリズムであるべき (SHOULD).
  • JWT alg Header Parameter が HS256, HS384 および HS512 のような MAC ベースのアルゴリズムを利用するならば, aud (audience) Claim に含まれる client_id に対応する client_secret の UTF-8 表現バイト列が署名の確認に用いられる. MAC ベースのアルゴリズムについて, aud が複数の値を持つとき, もしくは aud の値と異なる azp の値があるときの振る舞いは規定されない.
  • 現在時刻は exp Claim の時刻表現より前でなければならない (MUST).
  • iat Claim は現在時刻からはるか昔に発行されたトークンを拒絶するために利用でき, 攻撃を防ぐために nonce が保存される必要がある期間を制限する. 許容できる範囲は Client の仕様である.
  • nonce の値が Authentication Request にて送られたならば, nonce Claim が存在し, その値が Authentication Request にて送られたものと一致することを確認するためにチェックされなければならない (MUST). Client は nonce の値を リプレイアタックのためにチェックすべき (SHOULD). リプレイアタックを検知する正確な方法は Client の仕様である.
  • acr Claim が 要求されたならば, Client は主張された Claim の値が適切かどうかをチェックすべきである (SHOULD). acr Claim の値と意味はこの仕様の対象外である.
  • auth_time Claim が要求されたならば, この Claim のための特定のリクエストもしくは max_age パラメータを用いて Client は auth_time Claim の値をチェックし, もし最新のユーザー認証からあまりに長い時間が経過したと判定されたときは再認証を要求すべきである (SHOULD).

9. Client Authentication

ここでは, Client が Token Endpoint にアクセスする際に, Authorization Server に対して自身を認証する Client Authentication 方法を定義する. Client Registration で RP (Client) は Client Authentication 方法を登録できる (MAY). もし認証方法が登録されていない場合, デフォルトで client_secret_basic を用いる.

Client Authentication 方法は以下の通りである.

  • client_secret_basic
    • Authorization Server から client_secret 値を受け取った Client は, OAuth 2.0 [RFC6749] Section 3.2.1 に従い, HTTP Basic 認証スキーマを利用して Authorization Server に対して自身を認証する.
  • client_secret_post
    • Authorization Server から client_secret 値を受け取った Client は, OAuth 2.0 [RFC6749] Section 3.2.1 に従い, Client Credential をリクエストボディに含めて Authorization Server に対して自身を認証する.
  • client_secret_jwt
    • Authorization Server から client_secret 値を受け取った Client は, HMAC SHA-256 等の HMAC SHA アルゴリズムを利用して JWT を生成する. HMAC (Hash-based Message Authentication Code) は, client_secret の UTF-8 オクテットを共通鍵として利用して計算される.
    • Client は JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants [OAuth.JWT] および Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants [OAuth.Assertions] に従い認証を行う. JWT は以下の必須 (REQUIRED) の Claim Value を含まねばならず (MUST), 任意 (OPTIONAL) な Claim Value を含んでもよい (MAY).
      • iss: REQUIRED. Issuer. OAuth Client の client_id を含まねばならない (MUST).
      • sub: REQUIRED. Subject. OAuth Client の client_id を含まねばならない (MUST).
      • aud: REQUIRED. Audience. aud (audience) Claim. Authorization Server をオーディエンスとして指定する. Authorization Server は自身がオーディエンスに含まれることを検証しなければならない (MUST). Audience は Authorization Server の Token Endpoint URL にすべきである (SHOULD).
      • jti: REQUIRED. JWT ID. トークンごとにユニークな識別子. トークンの再利用を防ぐために利用される. これらのトークンは, 再利用条件について関係者での合意を得られていない限り, 2度以上利用しないようにしなければならない (MUST). そのような合意の詳細については本仕様の定めるところではない.
      • exp: REQUIRED. ID Token の有効期限. この時刻を超えて当該 ID Token を受理してはならない (MUST NOT).
      • iat: OPTIONAL. JWT 発行時刻. この JWT には, 他の Claim を含めても良い (MAY). 理解できない Claim は無視すること (MUST). 認証トークンは [OAuth.Assertions] の client_assertion パラメータとして送信すること (MUST). [OAuth.Assertions] の client_assertion_type パラメータには, [OAuth.JWT] の通りに "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" とすること.

  • private_key_jwt
    • 公開鍵を登録済の Client は, そのペアとなる秘密鍵で JWT に署名を行っても良い. Client は JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants [OAuth.JWT] および Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants [OAuth.Assertions] に従い認証すること. JWT は以下の必須 (REQUIRED) の Claim Value を含まねばならず (MUST), 任意 (OPTIONAL) な Claim Value を含んでもよい (MAY). JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants [OAuth.JWT] and Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants [OAuth.Assertions]. The JWT MUST contain the following REQUIRED Claim Values and MAY contain the following OPTIONAL Claim Values: -->
      • iss: REQUIRED. Issuer. OAuth Client の client_id を含まねばならない (MUST).
      • sub: REQUIRED. Subject. OAuth Client の client_id を含まねばならない (MUST).
      • aud: REQUIRED. Audience. aud (audience) Claim. Authorization Serve をオーディエンスとして指定する. Authorization Server は自身がオーディエンスに含まれることを検証しなければならない (MUST). Audience は Authorization Server の Token Endpoint URL にすべきである (SHOULD).
      • jti: REQUIRED. JWT ID. トークンごとにユニークな識別子. トークンの再利用を防ぐために利用される. これらのトークンは, 再利用条件について関係者での合意を得られていない限り, 2度以上利用しないようにしなければならない (MUST). そのような合意の詳細については本仕様の定めるところではない.
      • exp: REQUIRED. ID Token の有効期限. この時刻を超えて当該 ID Token を受理してはならない (MUST NOT).
      • iat: OPTIONAL. JWT 発行時刻.

この JWT には, 他の Claim を含めても良い (MAY). 理解できない Claim は無視すること (MUST). 認証トークンは [OAuth.Assertions] の client_assertion パラメータとして送信すること (MUST). [OAuth.Assertions] の client_assertion_type パラメータには, [OAuth.JWT] の通りに "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" とすること. 以下に例を示す. (改行は掲載上の都合による)

POST /token HTTP/1.1 Host: server.example.com Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code& code=i1WsRn1uB1& client_id=s6BhdRkqt3& client_assertion_type= urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer& client_assertion=PHNhbWxwOl ... ZT none Client は Token Endpoint での認証を行わない. これには, Implicit Flow を使う (よって Token Endpoint 自体を利用しない) 場合と, Token Endpoint は利用するが Client Secret を持たない Public Client である場合, およびその他の何らかの認証手段を用いる場合がある.

package openid
import (
"context"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/pkg/errors"
)
type IssuerURI string
const (
GoogleIssuerURI IssuerURI = "https://accounts.google.com"
LineIssuerURI IssuerURI = "https://access.line.me"
AppleIssuerURI IssuerURI = "https://appleid.apple.com"
)
func (i IssuerURI) String() string {
return string(i)
}
type ProviderName string
const (
ProviderLine ProviderName = "line"
ProviderApple ProviderName = "apple"
ProviderGoogle ProviderName = "google"
)
func (i ProviderName) IssuerURI() IssuerURI {
switch i {
case ProviderLine:
return LineIssuerURI
case ProviderApple:
return AppleIssuerURI
case ProviderGoogle:
return GoogleIssuerURI
default:
return ""
}
}
func (p ProviderName) String() string {
return string(p)
}
type Provider struct {
Issuer IssuerURI
AuthorizationEndpoint string
TokenEndpoint string
UserInfoEndpoint string
JwksURI string
}
var (
providers = map[IssuerURI]Provider{
GoogleIssuerURI: {
Issuer: GoogleIssuerURI,
AuthorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
TokenEndpoint: "https://oauth2.googleapis.com/token",
UserInfoEndpoint: "https://openidconnect.googleapis.com/v1/userinfo",
JwksURI: "https://www.googleapis.com/oauth2/v3/certs",
},
LineIssuerURI: {
Issuer: LineIssuerURI,
AuthorizationEndpoint: "https://access.line.me/oauth2/v2.1/authorize",
TokenEndpoint: "https://api.line.me/oauth2/v2.1/token",
UserInfoEndpoint: "https://api.line.me/v2/profile",
JwksURI: "https://api.line.me/oauth2/v2.1/certs",
},
AppleIssuerURI: {
Issuer: AppleIssuerURI,
AuthorizationEndpoint: "https://appleid.apple.com/auth/authorize",
TokenEndpoint: "https://appleid.apple.com/auth/token",
UserInfoEndpoint: "",
JwksURI: "https://appleid.apple.com/auth/keys",
},
}
cache = jwk.NewCache(context.Background())
)
func (p *Provider) GetJWKSet(ctx context.Context) (jwk.Set, error) {
return cache.Get(ctx, p.JwksURI)
}
func GetProvider(issuerURI IssuerURI) (Provider, bool) {
provider, ok := providers[issuerURI]
return provider, ok
}
func (p Provider) ClientID() string {
switch p.Issuer {
case LineIssuerURI:
return LINEClientID()
case AppleIssuerURI:
return ""
case GoogleIssuerURI:
return GoogleClientID()
default:
return ""
}
}
func (p Provider) ClientSecret() string {
switch p.Issuer {
case LineIssuerURI:
return ""
case AppleIssuerURI:
return ""
case GoogleIssuerURI:
return GoogleClientSecret()
default:
return ""
}
}
func (p Provider) Scopes() []string {
switch p.Issuer {
case LineIssuerURI:
return LINEScopes()
case AppleIssuerURI:
return []string{}
case GoogleIssuerURI:
return GoogleScopes()
default:
return []string{}
}
}
func (p *Provider) VerifyAccessToken(ctx context.Context, token string) error {
keyset, err := p.GetJWKSet(ctx)
if err != nil {
return errors.Wrap(err, "failed to get JWK set")
}
issuer := p.Issuer.String()
opts := []jwt.ParseOption{
jwt.WithKeySet(keyset),
jwt.WithIssuer(issuer),
}
_, err = jwt.Parse([]byte(token), opts...)
if err != nil {
return errors.Wrap(err, "failed to parse access token")
}
return nil
}
func (p *Provider) VerifyIDToken(ctx context.Context, token string) error {
keyset, err := p.GetJWKSet(ctx)
if err != nil {
return errors.Wrap(err, "failed to get JWK set")
}
clientID := p.ClientID()
issuer := p.Issuer.String()
opts := []jwt.ParseOption{
jwt.WithKeySet(keyset),
jwt.WithIssuer(issuer),
jwt.WithAudience(clientID),
}
_, err = jwt.Parse([]byte(token), opts...)
if err != nil {
return errors.Wrap(err, "failed to parse id token")
}
return nil
}
name: reviewdog
on: [pull_request]
jobs:
staticcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-go@v4
with:
go-version-file: ./src/go.mod
cache-dependency-path: ./src/go.sum
- uses: reviewdog/action-staticcheck@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workdir: ./src
reporter: github-pr-review
filter_mode: nofilter
fail_on_error: true
target: ./...
golangci-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: reviewdog/action-golangci-lint@v2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
workdir: ./src
reporter: github-pr-review
filter_mode: nofilter
fail_on_error: true
go_version_file: ./src/go.mod
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
package main
import (
myAws "auth-api/aws"
"auth-api/model"
"auth-api/util"
"context"
"flag"
"fmt"
"log"
"strings"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
idp "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
dynamodbTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
func main() {
sub := flag.String("sub", "e.g.) google:1232131231", "sns sub")
flag.Parse()
if sub == nil || *sub == "" {
log.Fatal("client id is required")
}
ctx := context.Background()
uid, err := getUID(ctx, *sub)
if err != nil {
log.Fatal(err)
}
if uid != "" {
auths, err := deleteUserAuthTable(ctx, uid)
if err != nil {
log.Fatal(err)
}
if err = deleteUserAttributeTable(ctx, uid); err != nil {
log.Fatal(err)
}
for _, auth := range auths {
if err = deleteUniqueKeyTable(ctx, auth.SID); err != nil {
log.Fatal(err)
}
}
if err = deleteCognitoUser(ctx, uid); err != nil {
log.Fatal(err)
}
}
}
func getUID(ctx context.Context, sub string) (string, error) {
client := myAws.GetDynamodbClient()
res, err := client.ExecuteStatement(ctx, &dynamodb.ExecuteStatementInput{
Statement: aws.String(fmt.Sprintf(`SELECT * FROM "%s" WHERE "sid" = ?`, myAws.GetDynamodbUserAuthTable())),
Parameters: []dynamodbTypes.AttributeValue{
&dynamodbTypes.AttributeValueMemberS{Value: sub},
},
})
if err != nil {
return "", err
}
if len(res.Items) <= 0 {
return "", fmt.Errorf("not found user auth table: sub = %s", sub)
}
var itemMap map[string]any
err = attributevalue.UnmarshalMap(res.Items[0], &itemMap)
if err != nil {
return "", err
}
var r model.UserAuth
err = util.MapToStruct(itemMap, &r)
if err != nil {
return "", err
}
return r.UID, nil
}
func deleteUserAuthTable(ctx context.Context, uid string) ([]model.UserAuth, error) {
client := myAws.GetDynamodbClient()
res, err := client.ExecuteStatement(ctx, &dynamodb.ExecuteStatementInput{
Statement: aws.String(fmt.Sprintf(`SELECT * FROM "%s" WHERE "uid" = ?`, myAws.GetDynamodbUserAuthTable())),
Parameters: []dynamodbTypes.AttributeValue{
&dynamodbTypes.AttributeValueMemberS{Value: uid},
},
})
if err != nil {
return nil, err
}
var result = make([]model.UserAuth, 0, 2)
for _, item := range res.Items {
var itemMap map[string]any
err = attributevalue.UnmarshalMap(item, &itemMap)
if err != nil {
return nil, err
}
fmt.Printf("delete itemMap: %+v\n", itemMap)
var r model.UserAuth
err := util.MapToStruct(itemMap, &r)
if err != nil {
return nil, err
}
result = append(result, r)
_, err = client.ExecuteStatement(ctx, &dynamodb.ExecuteStatementInput{
Statement: aws.String(fmt.Sprintf(`DELETE FROM "%s" WHERE "uid" = ? AND "sid" = ?`, myAws.GetDynamodbUserAuthTable())),
Parameters: []dynamodbTypes.AttributeValue{
&dynamodbTypes.AttributeValueMemberS{Value: r.UID},
&dynamodbTypes.AttributeValueMemberS{Value: r.SID},
},
})
if err != nil {
log.Printf("failed to delete user auth table: %w", err)
}
}
return result, nil
}
func deleteUserAttributeTable(ctx context.Context, uid string) error {
fmt.Println("delete user attribute uid: ", uid)
client := myAws.GetDynamodbClient()
_, err := client.ExecuteStatement(ctx, &dynamodb.ExecuteStatementInput{
Statement: aws.String(fmt.Sprintf(`DELETE FROM "%s" WHERE "uid" = ?`, myAws.GetDynamodbUserAttributeTable())),
Parameters: []dynamodbTypes.AttributeValue{
&dynamodbTypes.AttributeValueMemberS{Value: uid},
},
})
if err != nil {
log.Printf("failed to delete user attribute table: %w", err)
}
return nil
}
func deleteUniqueKeyTable(ctx context.Context, key string) error {
fmt.Println("key: ", key)
client := myAws.GetDynamodbClient()
if strings.HasPrefix(key, "google:") {
delKey := fmt.Sprintf(`user_auth_dev:{"sid":"%s"}`, key)
fmt.Println("delete user unique googl: ", delKey)
_, err := client.ExecuteStatement(ctx, &dynamodb.ExecuteStatementInput{
Statement: aws.String(fmt.Sprintf(`DELETE FROM "%s" WHERE "key" = ?`, myAws.GetDynamodbUniqueKeyTable())),
Parameters: []dynamodbTypes.AttributeValue{
&dynamodbTypes.AttributeValueMemberS{Value: delKey},
},
})
if err != nil {
log.Printf("failed to delete unique key table: %w", err)
}
return nil
} else if strings.HasPrefix(key, "email:") {
trimKey := strings.TrimPrefix(key, "email:")
keys := []string{
fmt.Sprintf(`user_auth_dev:{"sid":"%s"}`, key),
fmt.Sprintf(`user_attribute_dev:{"email":"%s"}`, trimKey),
fmt.Sprintf(`user_attribute_dev:{"email_not_verified":"%s"}`, trimKey),
}
for _, v := range keys {
fmt.Println("delete user unique email: ", v)
_, err := client.ExecuteStatement(ctx, &dynamodb.ExecuteStatementInput{
Statement: aws.String(fmt.Sprintf(`DELETE FROM "%s" WHERE "key" = ?`, myAws.GetDynamodbUniqueKeyTable())),
Parameters: []dynamodbTypes.AttributeValue{
&dynamodbTypes.AttributeValueMemberS{Value: v},
},
})
if err != nil {
log.Printf("failed to delete unique key table: %w", err)
}
}
return nil
} else if strings.HasPrefix(key, "hc:") {
trimKey := strings.TrimPrefix(key, "hc:")
keys := []string{
fmt.Sprintf(`user_auth_dev:{"sid":"%s"}`, key),
fmt.Sprintf(`user_attribute_dev:{"hc_id":"%s"}`, trimKey),
}
for _, key := range keys {
fmt.Println("delete user uniques healthcare id: ", key)
_, err := client.ExecuteStatement(ctx, &dynamodb.ExecuteStatementInput{
Statement: aws.String(fmt.Sprintf(`DELETE FROM "%s" WHERE "key" = ? `, myAws.GetDynamodbUniqueKeyTable())),
Parameters: []dynamodbTypes.AttributeValue{
&dynamodbTypes.AttributeValueMemberS{Value: key},
},
})
if err != nil {
log.Printf("failed to delete unique key table: %w", err)
}
}
return nil
}
keys := []string{
fmt.Sprintf(`user_auth_dev:{"sid":"%s"}`, key),
}
for _, key := range keys {
fmt.Println("delete user uniques id: ", key)
_, err := client.ExecuteStatement(ctx, &dynamodb.ExecuteStatementInput{
Statement: aws.String(fmt.Sprintf(`DELETE FROM "%s" WHERE "key" = ? `, myAws.GetDynamodbUniqueKeyTable())),
Parameters: []dynamodbTypes.AttributeValue{
&dynamodbTypes.AttributeValueMemberS{Value: key},
},
})
if err != nil {
log.Printf("failed to delete unique key table: %w", err)
}
}
return nil
}
func deleteCognitoUser(ctx context.Context, uid string) error {
idpClient := myAws.GetIdpClient()
input := &idp.AdminDeleteUserInput{
UserPoolId: aws.String(myAws.GetUserPoolID()),
Username: aws.String(uid),
}
if _, err := idpClient.AdminDeleteUser(ctx, input); err != nil {
return err
}
return nil
}
"secrets": [
{
"name": "HEALTHCARE_OAUTH2_APPLE_TEAM_ID",
"valueFrom": "arn:aws:secretsmanager:ap-northeast-1:239409977332:secret:dev-auth-api-g4mL2W:healthcareOauth2AppleTeamId::"
},
{
"name": "HEALTHCARE_OAUTH2_APPLE_KEY_ID",
"valueFrom": "arn:aws:secretsmanager:ap-northeast-1:239409977332:secret:dev-auth-api-g4mL2W:healthcareOauth2AppleKeyId::"
},
{
"name": "HEALTHCARE_OAUTH2_APPLE_SERVICE_ID",
"valueFrom": "arn:aws:secretsmanager:ap-northeast-1:239409977332:secret:dev-auth-api-g4mL2W:healthcareOauth2AppleServiceId::"
},
{
"name": "HEALTHCARE_OAUTH2_APPLE_PRIVATE_KEY",
"valueFrom": "arn:aws:secretsmanager:ap-northeast-1:239409977332:secret:dev-auth-api-g4mL2W:healthcareOauth2ApplePrivateKey::"
},
{
"name": "HEALTHCARE_OAUTH2_LINE_CHANNEL_ID",
"valueFrom": "arn:aws:secretsmanager:ap-northeast-1:239409977332:secret:dev-auth-api-g4mL2W:healthcareOauth2LineChannelId::"
},
{
"name": "HEALTHCARE_OAUTH2_GOOGLE_CLIENT_ID",
"valueFrom": "arn:aws:secretsmanager:ap-northeast-1:239409977332:secret:dev-auth-api-g4mL2W:healthcareOauth2GoogleClientId::"
},
{
"name": "HEALTHCARE_OAUTH2_GOOGLE_CLIENT_SECRET",
"valueFrom": "arn:aws:secretsmanager:ap-northeast-1:239409977332:secret:dev-auth-api-g4mL2W:healthcareOauth2GoogleClientSecret::"
}
],
type OpenIDConnectToken struct {
GrantType openid.GrantType `form:"grant_type" validate:"required,oneof=authorization_code refresh_token"`
ClientID string `form:"client_id"`
ClientSecret string `form:"client_secret"`
Code string `form:"code"`
RedirectURI string `form:"redirect_uri"`
RefreshToken string `form:"refresh_token"`
}
func NewOpenIDConnectToken(c echo.Context) (*OpenIDConnectToken, error) {
f := &OpenIDConnectToken{}
if err := c.Bind(f); err != nil {
return nil, echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
ctx := c.Request().Context()
if c.Request().Header.Get("Authorization") == "" {
validate.VarCtx(ctx, f.ClientID, "required")
validate.VarCtx(ctx, f.ClientSecret, "required")
}
if f.GrantType == openid.GrantTypeAuthorizationCode {
validate.VarCtx(ctx, f.Code, "required")
validate.VarCtx(ctx, f.RedirectURI, "required,uri")
} else if f.GrantType == openid.GrantTypeRefreshToken {
validate.VarCtx(ctx, f.RefreshToken, "required")
}
if err := validate.Struct(f); err != nil {
return nil, echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return f, nil
}
type GrantType string
const (
GrantTypeAuthorizationCode GrantType = "authorization_code"
GrantTypeRefreshToken GrantType = "refresh_token"
)
func (g GrantType) String() string {
return string(g)
}
func ToUserAttribute(entity dynamorm.Entity) (*UserAttribute, error) {
var result UserAttribute
attr := entity.Attributes()
v, err := json.Marshal(attr)
if err != nil {
return nil, e.Wrapf(err, "failed to marshal user attribute: %+v", attr)
}
if err := json.Unmarshal(v, &result); err != nil {
return nil, e.Wrapf(err, "failed to unmarshal user attribute entity: %s", string(v))
}
return &result, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment