こちらは バイセルテクノロジーズ Advent Calendar 2021 の 19 日目の記事です。
前日の記事は酒井さんの「React + TypeScript環境にGoogle Analyticsを導入してみた」でした。
こんにちは。開発 2 部の今井です。 バイセルでは主にサーバサイドの開発に携わっています。
本記事では、業務で検討する機会がありました「gqlgen と Auth0 を利用した認証機能」についてご紹介したいと思います。
- 今回ご紹介する認証機能について
- 主要な技術スタック(一部抜粋)
- リポジトリ
- 前提
- 必要な環境変数を定義
- GraphQL スキーマ定義
- GraphQL サーバの実装
- Resolver の実装
- ミューテーションの実行及び確認
- まとめ
- 最後に
- 参考にさせていただいたサイト
今回ご紹介する認証機能について
本システムは Auth0 を利用して認証認可の仕組みを組み込んでいます。
それを踏まえて、以下を考慮する必要がありました。
- 本システムを利用する場合はメールアドレスとパスワードが必要となる想定のもと設計する。
- ユーザの情報をDBへ保存後、招待メールを発行し、ユーザにパスワードを設定していただく必要があるためバックエンドの API から Auth0 に連携させる必要がある。
今回は DB への登録等は省略し、以下添付画像の認証部分に限り、検証した実装をご紹介します。
主要な技術スタック(一部抜粋)
- Docker Compose version 3.8
- Go: version 1.17
- 99designs/gqlgen version 0.13
- Auth0 Go SDK version 5.21.1
- sendgrid-go version 3.10.3
リポジトリ
今回利用するコードの全体は以下リポジトリにアップしております。
前提
以下を前提とし、話を進めていきます。
- Auth0 Machine to Machine アプリケーションが作成されていること
- SendGrid の API キーが発行されていること
※ 上記の手順は以下のリンクが参考になるかと思います。 auth0.com docs.sendgrid.com
必要な環境変数を定義
以下4つの環境変数を .env
ファイルに定義します。
Auth0 Management API
を利用するにあたって必要な環境変数は ダッシュボードの
Applications
> 作成した M2M API
> Settings
より確認できます。
同時にSendGrid の API キーも定義します。
AUTH0_DOMAIN AUTH0_CLIENT_ID AUTH0_CLIENT_SECRET SENDGRID_API_KEY
GraphQL スキーマ定義
今回は簡単にメールアドレスと名前をリクエストとし、Auth0 への登録時に生成されるユーザ ID をレスポンスとして返却します。
api/graph/schema/schema.gql
input UserInfo { email: String! familyName: String! givenName: String! } type Auth0UserID { Auth0UserID: ID! } type Mutation { registerUser(input: UserInfo!): Auth0UserID! }
GraphQL サーバの実装
gqlgen
を使ったサーバの実装、加えて Auth0 クライアントと SendGrid クライアントを作成します。
▶︎gqlgen_auth0_sample/server.go
func main() { ac, err := initAuth0Client() if err != nil { log.Fatalf("could not init auth0 client: %v", err) } mc, err := initMailerClient() if err != nil { log.Fatalf("could not init sendgrid client: %v", err) } srv := handler.NewDefaultServer( generated.NewExecutableSchema( generated.Config{ Resolvers: &resolver.Resolver{ Auth0Client: ac, MailClient: mc, }, }, ), ) // 中略 } func initAuth0Client() (*auth0.Client, error) { var ( domain = os.Getenv("AUTH0_DOMAIN") clientID = os.Getenv("AUTH0_CLIENT_ID") clientSecret = os.Getenv("AUTH0_CLIENT_SECRET") ) if domain == "" || clientID == "" || clientSecret == "" { return nil, fmt.Errorf("plese set env vars for auth0") } m, err := management.New(domain, management.WithClientCredentials(clientID, clientSecret)) if err != nil { return nil, fmt.Errorf("could not create an auth0 management: %w", err) } return auth0.NewClient(m), nil } func initMailerClient() (*mailer.Client, error) { var ( apiKey = os.Getenv("SENDGRID_API_KEY") ) if apiKey == "" { return nil, fmt.Errorf("please set env vars for sendgrid") } sg := sendgrid.NewSendClient(apiKey) return mailer.NewClient(sg), nil }
gqlgen_auth0_sample/internal/auth0/client.go
package auth0 // 中略 type Client struct { management *management.Management } func NewClient(mgmt *management.Management) *Client { return &Client{management: mgmt} }
gqlgen_auth0_sample/internal/mailer/client.go
package mailer // 中略 type Client struct { sg *sendgrid.Client } func NewClient(c *sendgrid.Client) *Client { return &Client{sg: c} }
Resolver の実装
ここからドメインロジックとなる部分の実装を進めていきます。 処理は大まかに以下の3つです。
① Auth0 Management API を使用し、Auth0 の DB にユーザを登録する。
② 作成されたユーザ ID をパラメータに、パスワード再設定用の URL を作成する。
③ パスワード再設定用の URL を SendGrid API を使用し、リクエストできたメールアドレスに送信する。
gqlgen_auth0_sample/internal/user/interface.go
package user // 中略 type Auth0ManagementClient interface { CreateUser(ctx context.Context, u *User) (string, error) CreateInvitationUrl(ctx context.Context, auth0UserID string) (string, error) } type MailClient interface { Send(ctx context.Context, from *mail.Address, to *mail.Address, subject string, message string) error }
gqlgen_auth0_sample/internal/graph/resolver/resolver.go
package resolver // 中略 type Resolver struct { Auth0Client user.Auth0ManagementClient MailClient user.MailClient }
gqlgen_auth0_sample/internal/user/user.go
package user // 中略 type User struct { Email string FamilyName string GivenName string } func (u *User) FullName() string { return strings.Trim(strings.Join([]string{u.FamilyName, u.GivenName}, " "), " ") }
gqlgen_auth0_sample/internal/graph/resolver/schema.resolvers.go
package resolver // 中略 func (r *mutationResolver) RegisterUser(ctx context.Context, input model.UserInfo) (*model.Auth0UserID, error) { var u = &user.User{ Email: input.Email, FamilyName: input.FamilyName, GivenName: input.GivenName, } res, err := user.RegisterUser(ctx, &user.RegisterUserInput{ User: u, Auth0ManagementClient: r.Auth0Client, MailClient: r.MailClient, }) if err != nil { return nil, fmt.Errorf("could not register user: %w", err) } return &model.Auth0UserID{ Auth0UserID: res, }, nil }
gqlgen_auth0_sample/internal/user/register_user.go
package user // 中略 // 送信元 var ( invitaionFrom = &mail.Address{ Name: "gqlgen-auth0", Address: "noreply@example.com", } invitaionSubject = "仮登録完了のお知らせ" ) type RegisterUserInput struct { User *User Auth0ManagementClient MailClient } func RegisterUser(ctx context.Context, input *RegisterUserInput) (string, error) { var ( amc = input.Auth0ManagementClient mc = input.MailClient ) auth0UserID, err := amc.CreateUser(ctx, input.User) if err != nil { return "", fmt.Errorf("failed to create user on auth0: %w", err) } invitaionUrl, err := amc.CreateInvitationUrl(ctx, auth0UserID) if err != nil { return "", fmt.Errorf("failed to send invitation: %w", err) } var ( body = invitaionUrl to = &mail.Address{ Name: input.User.FullName(), Address: input.User.Email, } ) if err := mc.Send(ctx, invitaionFrom, to, invitaionSubject, body); err != nil { return "", fmt.Errorf("failed to send invitation: %w", err) } return auth0UserID, nil }
① Auth0 Management API を使用し、Auth0 の DB にユーザを登録する。
この段階ではまだユーザのメールアドレスの認証とパスワードの設定が未完了のため、
management.User
構造体の EmailVerified
と VerifyEmail
のフィールドはともに false
で登録します。
またパスワード再設定用の URL を作成する際に生成されたユーザ ID が必要となる為、レスポンスとして返します。
※ connection
定数は以下の文言を参照しています。
gqlgen_auth0_sample/internal/auth0/client.go
package auth0 // 中略 const ( connection = "Username-Password-Authentication" invitationTTLSec = 86400 ) func (c *Client) CreateMember(ctx context.Context, u *user.User) (string , error) { // 招待メールからパスワードを設定するまでの間の仮パスワードを生成 pass, err := generateRandomPassword() if err != nil { return "", fmt.Errorf("could not generate password: %w", err) } managementUser := &management.User{ Connection: auth0.String(connection), Email: auth0.String(u.Email), Name: auth0.String(u.FullName()), Password: auth0.String(pass), EmailVerified: auth0.Bool(false), VerifyEmail: auth0.Bool(false), } if err := c.management.User.Create(managementUser, management.Context(ctx)); err != nil { return "", fmt.Errorf("could not create a user on auth0: %w", err) } return auth0.StringValue(managementUser.ID), nil } func generateRandomPassword() (string, error) { // 中略 }
② 作成されたユーザ ID をパラメータに、パスワード再設定用の URL を作成する。
management.Ticket
構造体の MarkEmailAsVerified
フィールドを true
にすることでメールアドレスの認証が完了となります。
またここでは定義していませんが、 ResultURL
フィールドに設定した URL にリダイレクトさせることも可能です。
gqlgen_auth0_sample/internal/auth0/client.go
package auth0 // 中略 func (c *Client) CreateInvitationUrl(ctx context.Context, auth0UserID string) (string, error) { t := &management.Ticket{ UserID: auth0.String(auth0UserID), TTLSec: auth0.Int(invitationTTLSec), MarkEmailAsVerified: auth0.Bool(true), } if err := c.management.Ticket.ChangePassword(t, management.Context(ctx)); err != nil { return "", fmt.Errorf("could not create ticket for change password: %w", err) } return auth0.StringValue(t.Ticket) + "invite", nil }
③ パスワード再設定用の URL を SendGrid API を使用し、リクエストできたメールアドレスに送信する。
gqlgen_auth0_sample/internal/mailer/client.go
package mailer // 中略 func (c *Client) Send(ctx context.Context, from *mail.Address, to *mail.Address, subject string, message string) error { msg := sgmail.NewSingleEmail( sgmail.NewEmail(from.Name, from.Address), subject, sgmail.NewEmail(to.Name, to.Address), subject, message) res, err := c.sg.SendWithContext(ctx, msg) if err != nil { return err } // OK(200) ではなく、Accepted(201) が返却される if res.StatusCode != http.StatusAccepted { fmt.Fprintf(os.Stderr, res.Body) return fmt.Errorf("unexpected status from sendgrid api: %d", res.StatusCode) } return nil }
ミューテーションの実行及び確認
ここまでで必要な処理は揃いましたので動作確認をします。
サーバを起動する。
$ docker compose up
GraphQL Playground (http://localhost:8081)を開き、
registerUser
ミューテーションを実行する。
email
フィールドは自分のメールアドレスを定義しています。
実行すると Auth0 への登録時に生成されたユーザ ID が返却されていることが確認できますね。Auth0 にユーザが登録されていることを確認する。
確認する際には Auth0 のダッシュボードより、User Management
>Users
を開きます。
2 で返却されたユーザIDと同様のIDを持つユーザの登録が確認できます。
またDetails
に記載されているIdentity Provider Attributes
の情報は、ID Token
やAccess Token
の情報としても流用できます。
招待メールが届いていることを確認する。
届いている URL を開きます。パスワードの再設定をし、変更されることを確認する。
5 で設定したパスワードを利用し、実際にログインできることを確認する。
確認する際には Auth0 のダッシュボードより、Authentication
>Database
>Database Connections
>Try
を開きます。 パスワード入力画面が表示されるので、パスワードを入力します。
ログインが成功すると以下の画面が表示されます。
リクエストで送った名前と、レスポンスとして返却された Auth0 のユーザ ID が確認できますね。
まとめ
いかがでしたでしょうか?
実運用では Auth0 へ登録する前段でユーザ情報を DB へ保存する、リダイレクト先は FE のアプリケーションにする等の処理が必要となってくるかとは思いますが、今回ご紹介した認証機能の部分が全体の肝となるためご紹介しました。
gqlgen や Auth0 を利用した認証機能の参考となれば幸いです。
最後に
バイセルでは一緒に新規サービスの開発をしてくれるエンジニアを募集しています。
明日のバイセルテクノロジーズ Advent Calendar 2021は、
玉利さんによる「内定者インターンが行った社内サービスのデザイン刷新」です。