メインコンテンツまでスキップ

あなたの Go Web アプリケーションに認証 (Authentication) を追加する

このガイドでは、Logto をあなたの Go Web アプリケーションに統合する方法を紹介します。

ヒント:
  • 以下のデモは Gin Web Framework を基盤としています。同じ手順で他のフレームワークにも Logto を統合できます。
  • Go のサンプルプロジェクトは Go SDK リポジトリ で利用できます。

前提条件

インストール

プロジェクトのルートディレクトリで次のコマンドを実行します:

# 事前定義された値や型へアクセスするためのコアパッケージをインストール
go get github.com/logto-io/go/v2/core

# Logto と連携するためのクライアントパッケージをインストール
go get github.com/logto-io/go/v2/client

アプリケーションコードに github.com/logto-io/go/v2/core および github.com/logto-io/go/v2/client パッケージを追加します:

main.go
// main.go
package main

import (
"github.com/gin-gonic/gin"
// 依存関係を追加
"github.com/logto-io/go/v2/core"
"github.com/logto-io/go/v2/client"
)

func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(200, "Hello Logto!")
})
router.Run(":3000")
}

統合

セッションストレージを作成する

従来の Web アプリケーションでは、ユーザーの認証 (Authentication) 情報はユーザーセッションに保存されます。

Logto SDK では Storage インターフェースが提供されており、Web フレームワークに合わせて Storage アダプターを実装することで、Logto SDK がユーザーの認証 (Authentication) 情報をセッションに保存できるようになります。

注記:

Logto によって保存されるユーザー認証 (Authentication) 情報はクッキーのサイズ制限を超える可能性があるため、クッキー ベースのセッションの使用は推奨しません。 この例ではメモリ ベースのセッションを使用しています。実運用では必要に応じて Redis や MongoDB などの技術を使ってセッションを保存できます。

Logto SDK の Storage 型は次のとおりです:

storage.go
package client

type Storage interface {
GetItem(key string) string
SetItem(key, value string)
}

github.com/gin-contrib/sessions ミドルウェアを例として、このプロセスを説明します。

ミドルウェアをアプリケーションに適用することで、ルートハンドラー内でユーザーリクエストコンテキストからユーザーセッションを取得できます:

main.go
package main

import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore"
"github.com/gin-gonic/gin"
"github.com/logto-io/go/v2/client"
)

func main() {
router := gin.Default()

// この例ではメモリ ベースのセッションを使用します
store := memstore.NewStore([]byte("your session secret"))
router.Use(sessions.Sessions("logto-session", store))

router.GET("/", func(ctx *gin.Context) {
// ユーザーセッションを取得
session := sessions.Default(ctx)
// ...
ctx.String(200, "Hello Logto!")
})
router.Run(":3000")
}

session_storage.go ファイルを作成し、SessionStorage を定義して Logto SDK の Storage インターフェースを実装します:

session_storage.go
package main

import (
"github.com/gin-contrib/sessions"
)

type SessionStorage struct {
session sessions.Session
}

func (storage *SessionStorage) GetItem(key string) string {
value := storage.session.Get(key)
if value == nil {
return ""
}
return value.(string)
}

func (storage *SessionStorage) SetItem(key, value string) {
storage.session.Set(key, value)
storage.session.Save()
}

これで、ルートハンドラー内で Logto 用のセッションストレージを作成できます:

session := sessions.Default(ctx)
sessionStorage := &SessionStorage{session: session}

LogtoClient を初期化する

まず、Logto の設定を作成します:

main.go
func main() {
// ...
logtoConfig := &client.LogtoConfig{
Endpoint: "<your-logto-endpoint>", // 例: http://localhost:3001
AppId: "<your-application-id>",
AppSecret: "<your-application-secret>",
}
// ...
}
ヒント:

「App Secret」は管理コンソールのアプリケーション詳細ページから見つけてコピーできます:

App Secret

次に、上記の Logto 設定を使用して、各ユーザーリクエストに対して LogtoClient を作成できます:

main.go
func main() {
// ...

router.GET("/", func(ctx *gin.Context) {
// LogtoClient を作成
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(
logtoConfig,
&SessionStorage{session: session},
)

// Logto を使用してホームページのコンテンツを制御
authState := "このウェブサイトにログインしていません。 :("

if logtoClient.IsAuthenticated() {
authState = "このウェブサイトにログインしています! :)"
}

homePage := `<h1>Hello Logto</h1>` +
"<div>" + authState + "</div>"

ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage))
})

// ...
}

アプリを設定する

詳細に入る前に、エンドユーザー体験の概要を簡単にご紹介します。サインインプロセスは次のようにシンプルにまとめられます:

  1. アプリがサインインメソッドを呼び出します。
  2. ユーザーは Logto のサインインページにリダイレクトされます。ネイティブアプリの場合は、システムブラウザが開かれます。
  3. ユーザーがサインインし、アプリ(リダイレクト URI として設定)に戻されます。

リダイレクトベースのサインインについて

  1. この認証 (Authentication) プロセスは OpenID Connect (OIDC) プロトコルに従い、Logto はユーザーのサインインを保護するために厳格なセキュリティ対策を講じています。
  2. 複数のアプリがある場合、同じアイデンティティプロバイダー (Logto) を使用できます。ユーザーがあるアプリにサインインすると、Logto は別のアプリにアクセスした際に自動的にサインインプロセスを完了します。

リダイレクトベースのサインインの理論と利点について詳しく知るには、Logto サインイン体験の説明を参照してください。


注記:

以下のコードスニペットでは、あなたのアプリが http://localhost:3000/ で実行されていると仮定しています。

リダイレクト URI を設定する

Logto Console のアプリケーション詳細ページに移動します。リダイレクト URI http://localhost:3000/callback を追加します。

Logto Console のリダイレクト URI

サインインと同様に、ユーザーは共有セッションからサインアウトするために Logto にリダイレクトされるべきです。完了したら、ユーザーをあなたのウェブサイトに戻すと良いでしょう。例えば、http://localhost:3000/ をサインアウト後のリダイレクト URI セクションとして追加します。

その後、「保存」をクリックして変更を保存します。

リダイレクトを処理する

ユーザーが Logto のサインインページで正常にサインインすると、Logto はユーザーをリダイレクト URI にリダイレクトします。

リダイレクト URI が http://localhost:3000/callback であるため、サインイン後のコールバックを処理するために /callback ルートを追加します。

main.go
func main() {
// ...

// サインインコールバックリクエストを処理するためのルートを追加
router.GET("/callback", func(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(
logtoConfig,
&SessionStorage{session: session},
)

// サインインコールバックリクエストは Logto によって処理されます
err := logtoClient.HandleSignInCallback(ctx.Request)
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

// 開発者が指定したページにジャンプします。
// この例では、ユーザーをホームページに戻します。
ctx.Redirect(http.StatusTemporaryRedirect, "/")
})

// ...
}

サインインルートを実装する

リダイレクト URI が設定された後、サインインリクエストを処理するために sign-in ルートを追加し、ホームページにサインインリンクも追加します:

main.go
func main() {
// ...

// ホームページにサインインリクエストを実行するリンクを追加
router.GET("/", func(ctx *gin.Context) {
// ...
homePage := `<h1>Hello Logto</h1>` +
"<div>" + authState + "</div>" +
// リンクを追加
`<div><a href="/sign-in">Sign In</a></div>`

ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage))
})

// サインインリクエストを処理するためのルートを追加
router.GET("/sign-in", func(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(
logtoConfig,
&SessionStorage{session: session},
)

// サインインリクエストは Logto によって処理されます。
// ユーザーはサインイン後にリダイレクト URI にリダイレクトされます。
signInUri, err := logtoClient.SignIn("http://localhost:3000/callback")
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

// ユーザーを Logto サインインページにリダイレクトします。
ctx.Redirect(http.StatusTemporaryRedirect, signInUri)
})

// ...
}

これで、ユーザーが http://localhost:3000/sign-in にアクセスすると、Logto サインインページにリダイレクトされます。

サインアウトルートを実装する

ユーザーがサインアウトすると、Logto はユーザーをポストサインアウトリダイレクト URI にリダイレクトします。

次に、sign-out ルートを追加してサインアウトリクエストを処理し、ホームページにサインアウトリンクを追加しましょう:

main.go
func main() {
// ...

// ホームページにサインアウトリクエストを実行するリンクを追加
router.GET("/", func(ctx *gin.Context) {
// ...
homePage := `<h1>Hello Logto</h1>` +
"<div>" + authState + "</div>" +
`<div><a href="/sign-in">Sign In</a></div>` +
// リンクを追加
`<div><a href="/sign-out">Sign Out</a></div>`

ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage))
})

// サインアウトリクエストを処理するルートを追加
router.GET("/sign-out", func(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(
logtoConfig,
&SessionStorage{session: session},
)

// サインアウトリクエストは Logto によって処理されます。
// サインアウト後、ユーザーはポストサインアウトリダイレクト URI にリダイレクトされます。
signOutUri, signOutErr := logtoClient.SignOut("http://localhost:3000")

if signOutErr != nil {
ctx.String(http.StatusOK, signOutErr.Error())
return
}

ctx.Redirect(http.StatusTemporaryRedirect, signOutUri)
})

// ...
}

ユーザーがサインアウトリクエストを行った後、Logto はセッション内のすべてのユーザー認証情報をクリアします。

チェックポイント: アプリケーションをテストする

これで、アプリケーションをテストできます:

  1. アプリケーションを実行すると、サインインボタンが表示されます。
  2. サインインボタンをクリックすると、SDK がサインインプロセスを初期化し、Logto のサインインページにリダイレクトされます。
  3. サインインすると、アプリケーションに戻り、サインアウトボタンが表示されます。
  4. サインアウトボタンをクリックして、トークンストレージをクリアし、サインアウトします。

ユーザー情報を取得する

ユーザー情報を表示する

ユーザーの情報を表示するには、client.GetIdTokenClaims メソッドを使用できます。例えば、ルートを追加します:

main.go
func main() {
//...

router.GET("/user-id-token-claims", func(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(logtoConfig, &SessionStorage{session: session})

idTokenClaims, err := logtoClient.GetIdTokenClaims()

if err != nil {
ctx.String(http.StatusOK, err.Error())
}

ctx.JSON(http.StatusOK, idTokenClaims)
})
}

追加のクレーム (Claims) をリクエストする

client.GetIdTokenClaims() から返されるオブジェクトに一部のユーザー情報が欠けていることがあります。これは、OAuth 2.0 と OpenID Connect (OIDC) が最小特権の原則 (PoLP) に従うように設計されており、Logto はこれらの標準に基づいて構築されているためです。

デフォルトでは、限られたクレーム (Claims) が返されます。より多くの情報が必要な場合は、追加のスコープ (Scopes) をリクエストして、より多くのクレーム (Claims) にアクセスできます。

備考:

「クレーム (Claim)」はサブジェクトについての主張であり、「スコープ (Scope)」はクレーム (Claims) のグループです。現在のケースでは、クレーム (Claim) はユーザーに関する情報の一部です。

スコープ (Scope) とクレーム (Claim) の関係の非規範的な例を示します:

ヒント:

「sub」クレーム (Claim) は「サブジェクト (Subject)」を意味し、ユーザーの一意の識別子(つまり、ユーザー ID)です。

Logto SDK は常に 3 つのスコープ (Scopes) をリクエストします:openidprofile、および offline_access

追加のスコープをリクエストするには、スコープを LogtoConfig オブジェクトに渡すことができます。例えば:

main.go
logtoConfig := &client.LogtoConfig{
// ...other configs
Scopes: []string{"email", "phone"},
}

その後、client.GetIdTokenClaims() の戻り値で追加のクレーム (Claims) にアクセスできます:

idTokenClaims, error := client.GetIdTokenClaims()
// これで追加のクレーム `claims.email`、`claims.phone` などにアクセスできます。

ネットワークリクエストが必要なクレーム (Claims)

ID トークンの肥大化を防ぐために、一部のクレーム (Claims) は取得するためにネットワークリクエストが必要です。例えば、custom_data クレームはスコープで要求されてもユーザーオブジェクトに含まれません。これらのクレームにアクセスするには、 client.FetchUserInfo() メソッドを使用できます

userInfo, error := client.FetchUserInfo()
// これでクレーム `userInfo.custom_data` にアクセスできます。
このメソッドは、userinfo エンドポイントにリクエストを送信してユーザー情報を取得します。利用可能なスコープとクレームについて詳しくは、スコープとクレームのセクションを参照してください。

スコープとクレーム (Claims)

Logto は OIDC の スコープ (Scope) とクレーム (Claim) の規約 を使用して、ID トークンおよび OIDC userinfo エンドポイント からユーザー情報を取得するためのスコープ (Scope) とクレーム (Claim) を定義しています。「スコープ (Scope)」と「クレーム (Claim)」は、OAuth 2.0 および OpenID Connect (OIDC) 仕様の用語です。

標準の OIDC クレーム (Claim) については、ID トークンへの含有はリクエストされたスコープ (Scope) によって厳密に決定されます。拡張クレーム (Claim)(例:custom_dataorganizations)は、カスタム ID トークン 設定を通じて ID トークンに追加で表示するように構成できます。

こちらはサポートされているスコープと対応するクレーム (Claims) の一覧です:

標準 OIDC スコープ

openid(デフォルト)

Claim nameType説明
substringユーザーの一意の識別子

profile(デフォルト)

Claim nameType説明
namestringユーザーのフルネーム
usernamestringユーザー名
picturestringエンドユーザーのプロフィール画像の URL。この URL は画像ファイル(例:PNG、JPEG、GIF 画像ファイル)を指す必要があり、画像を含む Web ページではありません。この URL は、エンドユーザーを説明する際に表示するのに適したプロフィール写真を特に参照するべきであり、エンドユーザーが撮影した任意の写真ではありません。
created_atnumberエンドユーザーが作成された時刻。Unix エポック(1970-01-01T00:00:00Z)からのミリ秒数で表されます。
updated_atnumberエンドユーザー情報が最後に更新された時刻。Unix エポック(1970-01-01T00:00:00Z)からのミリ秒数で表されます。

その他の 標準クレーム (Standard Claims) には、family_namegiven_namemiddle_namenicknamepreferred_usernameprofilewebsitegenderbirthdatezoneinfolocale などがあり、これらも profile スコープに含まれます(userinfo エンドポイントをリクエストする必要はありません)。上記のクレームとの違いは、これらのクレームは値が空でない場合のみ返される点です。一方、上記のクレームは値が空の場合 null が返されます。

注記:

標準クレーム (Standard Claims) とは異なり、created_at および updated_at クレームは秒ではなくミリ秒を使用しています。

email

Claim nameType説明
emailstringユーザーのメールアドレス
email_verifiedbooleanメールアドレスが認証済みかどうか

phone

Claim nameType説明
phone_numberstringユーザーの電話番号
phone_number_verifiedboolean電話番号が認証済みかどうか

address

アドレスクレームの詳細については OpenID Connect Core 1.0 を参照してください。

備考:

(デフォルト) と記載されたスコープは常に Logto SDK によってリクエストされます。標準 OIDC スコープ下のクレーム (Claims) は、対応するスコープがリクエストされた場合、常に ID トークン (ID token) に含まれます — 無効化できません。

拡張スコープ

以下のスコープは Logto によって拡張されており、userinfo エンドポイント を通じてクレーム (Claims) を返します。これらのクレームは Console > Custom JWT を通じて ID トークン (ID token) に直接含めるよう設定することもできます。詳細は カスタム ID トークン を参照してください。

custom_data

Claim nameType説明デフォルトで ID トークンに含まれるか
custom_dataobjectユーザーのカスタムデータ

identities

Claim nameType説明デフォルトで ID トークンに含まれるか
identitiesobjectユーザーのリンク済みアイデンティティ
sso_identitiesarrayユーザーのリンク済み SSO アイデンティティ

roles

Claim nameType説明デフォルトで ID トークンに含まれるか
rolesstring[]ユーザーのロール

urn:logto:scope:organizations

Claim nameType説明デフォルトで ID トークンに含まれるか
organizationsstring[]ユーザーが所属する組織 ID
organization_dataobject[]ユーザーが所属する組織データ
注記:

これらの組織クレーム (Organization Claims) は、不透明トークン (Opaque token) を使用している場合でも userinfo エンドポイント経由で取得できます。ただし、不透明トークン (Opaque token) は組織トークン (Organization token) として組織固有リソースへのアクセスには使用できません。詳細は 不透明トークン (Opaque token) と組織 (Organizations) を参照してください。

urn:logto:scope:organization_roles

Claim nameType説明デフォルトで ID トークンに含まれるか
organization_rolesstring[]ユーザーが所属する組織ロール(<organization_id>:<role_name> 形式)

API リソースと組織

まず 🔐 ロールベースのアクセス制御 (RBAC) を読むことをお勧めします。これにより、Logto の RBAC の基本概念と API リソースを適切に設定する方法を理解できます。

Logto クライアントを設定する

API リソースを設定したら、アプリで Logto を設定する際にそれらを追加できます:

logtoConfig := &client.LogtoConfig{
// ...other configs
Resources: []string{"https://shopping.your-app.com/api", "https://store.your-app.com/api"},
}

各 API リソースには独自の権限 (スコープ) があります。

例えば、https://shopping.your-app.com/api リソースには shopping:readshopping:write の権限があり、https://store.your-app.com/api リソースには store:readstore:write の権限があります。

これらの権限を要求するには、アプリで Logto を設定する際にそれらを追加できます:

logtoConfig := &client.LogtoConfig{
// ...other configs
Scopes: []string{"shopping:read", "shopping:write", "store:read", "store:write"},
Resources: []string{"https://shopping.your-app.com/api", "https://store.your-app.com/api"},
}

スコープが API リソースとは別に定義されていることに気付くかもしれません。これは、OAuth 2.0 のリソースインジケーター が、リクエストの最終的なスコープはすべてのターゲットサービスでのすべてのスコープの直積になると指定しているためです。

したがって、上記のケースでは、Logto での定義からスコープを簡略化できます。両方の API リソースは、プレフィックスなしで readwrite スコープを持つことができます。その後、Logto の設定では:

logtoConfig := &client.LogtoConfig{
// ...other configs
Scopes: []string{"read", "write"},
Resources: []string{"https://shopping.your-app.com/api", "https://store.your-app.com/api"},
}

各 API リソースは、readwrite の両方のスコープを要求します。

注記:

API リソースで定義されていないスコープを要求しても問題ありません。例えば、API リソースに email スコープが利用できなくても、email スコープを要求できます。利用できないスコープは安全に無視されます。

サインインが成功すると、Logto はユーザーのロールに応じて適切なスコープを API リソースに発行します。

API リソースのためのアクセス トークンを取得する

特定の API リソースのアクセス トークンを取得するには、GetAccessToken メソッドを使用できます:

accessToken, error := logtoClient.GetAccessToken("https://shopping.your-app.com/api")

このメソッドは、ユーザーが関連する権限を持っている場合に API リソースにアクセスするために使用できる JWT アクセス トークンを返します。現在キャッシュされているアクセス トークンが期限切れの場合、このメソッドは自動的にリフレッシュ トークンを使用して新しいアクセス トークンを取得しようとします。

組織トークンを取得する

組織 (Organization) が初めての場合は、🏢 組織 (マルチテナンシー) を読んで始めてください。

Logto クライアントを設定する際に、core.UserScopeOrganizations スコープを追加する必要があります:

logtoConfig := &client.LogtoConfig{
// ...other configs
Scopes: []string{core.UserScopeOrganizations},
}

ユーザーがサインインしたら、ユーザーのための組織トークンを取得できます:

// パラメーターを有効な組織 ID に置き換えてください。
// ユーザーの有効な組織 ID は、ID トークンのクレーム `organizations` にあります。
accessToken, error := logtoClient.GetOrganizationToken("organization-id")

// または
accessTokenClaims, error := logtoClient.GetOrganizationTokenClaims("organization-id")

組織 API リソース

組織内の API リソース用のアクセス トークンを取得するには、GetAccessTokenWithOptions メソッドを使用し、API リソースと組織 ID の両方をパラメーターとして指定します:

accessToken, error := client.GetAccessTokenWithOptions(
client.GetAccessTokenOptions{
Resource: 'https://shopping.your-app.com/api',
OrganizationId: organizationId,
},
);

さらなる読み物

エンドユーザーフロー:認証 (Authentication) フロー、アカウントフロー、組織フロー コネクターの設定 認可 (Authorization)