メインコンテンツに移動

Go言語OAuth 2.0実装ガイド|認可コード・PKCE・トークン管理

Go言語でWebアプリケーションやAPIを作るとき、外部サービス連携、会員ログイン、管理画面、モバイル連携、マイクロサービス間通信などでOAuth 2.0を扱う場面は多くあります。OAuth 2.0は「利用者のパスワードをアプリケーションへ渡さず、限定された権限で保護された資源へアクセスさせる」ための認可の枠組みです。OAuth 2.0の中核仕様であるRFC 6749は、認可サーバー、資源サーバー、クライアント、資源所有者という役割と、認可付与を使ってアクセストークンを取得する流れを定義しています。

ただし、OAuth 2.0は便利な一方で、実装を間違えると危険です。リダイレクトURIの検証不足、状態値の不足、トークンの漏えい、秘密値の直書き、リフレッシュトークンの保存ミス、認証と認可の混同などは、Go言語の実装でも起こり得ます。2025年に発行されたRFC 9700は、OAuth 2.0の現在の安全な実装慣行をまとめ、RFC 6749やBearer Token Usageの安全助言を更新・拡張する文書として位置付けられています。

この記事では、Go言語でOAuth 2.0を実装するための実践的な設計を解説します。単なるログインボタンの作り方ではなく、認可コードフロー、PKCE、アクセストークン検証、リフレッシュトークン管理、OpenID Connectとの違い、API保護、テスト、運用監視まで含めて、実務で安全に使える構成を目指します。

1. OAuth 2.0とは

OAuth 2.0とは、保護された資源へのアクセスを第三者アプリケーションへ限定的に許可するための認可フレームワークです。利用者のパスワードをアプリケーションへ直接渡さず、認可サーバーが発行するアクセストークンを使ってAPIへアクセスします。

Go言語のWebアプリケーションでは、外部プロバイダーへのログイン連携、Google APIの利用、社内認可基盤との接続、APIゲートウェイの保護などにOAuth 2.0が使われます。ここで重要なのは、OAuth 2.0は本来「認可」の仕組みであり、本人確認そのものを標準化する場合はOpenID Connectを組み合わせる点です。

1.1 認可のための仕組み

OAuth 2.0は、利用者が持つ資源へのアクセス権限を、別のアプリケーションへ委任するために使います。たとえば、Go製の業務アプリが利用者の同意を得てカレンダーAPIへアクセスする場合、利用者のパスワードではなくアクセストークンを使います。

この設計により、利用者はパスワードを共有せずに、必要な範囲だけアクセスを許可できます。アプリケーション側も、利用者の長期的な秘密情報を保持しなくて済むため、安全性と運用性を高めやすくなります。

1.2 4つの主要な役割

OAuth 2.0では、資源所有者、クライアント、認可サーバー、資源サーバーという役割が登場します。RFC 6749は、これらの役割を前提に、クライアントが認可付与を使ってアクセストークンを取得し、資源サーバーへアクセスする流れを定義しています。

Go言語で実装する場合、自分のアプリケーションがどの役割なのかを最初に決める必要があります。外部APIを呼び出すWebアプリならクライアント、APIを保護する側なら資源サーバー、自社でトークンを発行するなら認可サーバーの実装領域になります。

1.3 アクセストークンの役割

アクセストークンは、保護されたAPIへアクセスするための資格情報です。Bearer Tokenの場合、トークンを持っている者が利用できる性質を持ち、RFC 6750はBearer Tokenを「保持者が暗号鍵の所有証明なしに使える安全トークン」と説明しています。

そのため、アクセストークンはパスワードに近い慎重さで扱う必要があります。Goのログ、エラー出力、URL、解析ツール、ブラウザ保存領域などに不用意に出してはいけません。

1.4 認証との違い

OAuth 2.0は認可の仕組みです。つまり「このアプリケーションに、この範囲のAPI利用を許可する」という話であり、「この利用者が誰か」を標準的に証明する仕組みではありません。

利用者の本人確認を標準的に扱いたい場合は、OpenID Connectを使います。OpenID Connect Core 1.0は、OAuth 2.0の上に作られた識別層であり、認可サーバーによる認証結果をクライアントが検証し、利用者の基本情報を取得できる仕組みとして説明されています。

1.5 Go言語で扱う範囲

Go言語では、OAuth 2.0クライアント、API保護用のミドルウェア、トークン検証、外部API呼び出し、セッション管理を実装することが多いです。golang.org/x/oauth2 は、OAuth 2.0仕様に基づいた認可済みHTTP要求を作るためのパッケージとして提供されています。

一方で、認可サーバー自体を自前実装する場合は難易度が上がります。トークン発行、同意画面、クライアント登録、リダイレクトURI検証、鍵管理、失効、監査まで必要になるため、まずは既存の認可基盤を使う設計が現実的です。

2. Go言語OAuth 2.0実装の全体像

Go言語でOAuth 2.0を扱うときは、最初に「ログイン連携をしたいのか」「外部APIを呼びたいのか」「自分のAPIを保護したいのか」を分けます。目的が違えば、実装すべき処理も保管すべき情報も変わります。

Webアプリケーションでは、認可URLへリダイレクトし、戻ってきた認可コードをトークンへ交換し、アクセストークンを使ってAPIを呼びます。APIサーバーでは、受け取ったBearer Tokenを検証し、権限範囲を確認してから処理を実行します。

2.1 クライアント実装

クライアント実装では、Goアプリケーションが認可サーバーへ利用者を案内し、認可コードを受け取り、トークンへ交換します。外部プロバイダー連携や、社内ID基盤を使うWebアプリでよく使われます。

この場合、重要なのは状態値、リダイレクトURI、クライアント秘密値、トークン保存です。状態値を使わない実装は、横取りや不正なコールバックに弱くなります。

2.2 資源サーバー実装

資源サーバー実装では、GoのAPIが受け取ったアクセストークンを検証します。検証後、トークンの発行者、有効期限、対象者、権限範囲を確認して、処理を許可します。

トークン検証を単なる文字列比較にしてはいけません。JWTであれば署名検証、期限確認、発行者確認、対象者確認が必要であり、不透明トークンであれば認可サーバーの検査端点を使う設計になります。

2.3 セッション管理

Webアプリケーションでは、OAuth 2.0で取得した情報をそのままブラウザへ出すのではなく、サーバー側セッションとして管理することが多いです。アクセストークンをブラウザに直接保存すると、漏えい時の影響が大きくなります。

Goでは、HTTP専用、Secure属性、SameSite属性を持つCookieを使い、サーバー側にセッションを置く構成が扱いやすいです。認可結果とWebセッションは分けて考えることが重要です。

2.4 環境変数と秘密値

クライアントID、クライアント秘密値、認可URL、トークンURL、リダイレクトURIは環境ごとに変わります。Goコードへ直書きせず、環境変数や秘密情報管理サービスから読み込みます。

特にクライアント秘密値は、Gitリポジトリ、コンテナ画像、ログに混入させてはいけません。漏えいした場合は、認可サーバー側でローテーションできる運用を用意します。

2.5 最小構成例

以下は、GoアプリケーションでOAuth 2.0設定を作る最小例です。実務では、状態値、PKCE、セッション、エラー処理、トークン保存を必ず追加します。

Go設定例

 

package main import ( "os" "golang.org/x/oauth2" ) func oauthConfig() *oauth2.Config { return &oauth2.Config{ ClientID:     os.Getenv("OAUTH_CLIENT_ID"), ClientSecret: os.Getenv("OAUTH_CLIENT_SECRET"), RedirectURL:  os.Getenv("OAUTH_REDIRECT_URL"), Scopes: []string{ "profile", "email", }, Endpoint: oauth2.Endpoint{ AuthURL:  os.Getenv("OAUTH_AUTH_URL"), TokenURL: os.Getenv("OAUTH_TOKEN_URL"), }, } }

 

3. 認可コードフロー

認可コードフローは、GoのサーバーサイドWebアプリケーションで最もよく使われるOAuth 2.0の流れです。利用者を認可サーバーへリダイレクトし、認可後に短命の認可コードを受け取り、それをサーバー側でアクセストークンへ交換します。

RFC 6749は、認可コード付与をアクセストークンとリフレッシュトークンを取得するために使う方式として説明し、機密クライアントに最適化された流れとしています。

3.1 認可URL生成

最初にGoアプリケーションは、認可サーバーへ利用者を送るURLを生成します。このURLには、クライアントID、リダイレクトURI、応答種別、権限範囲、状態値などを含めます。

状態値は、コールバックが自分の開始した認可要求に対応しているかを確認するために使います。セッションに保存した状態値と、戻ってきた状態値が一致しない場合は拒否します。

3.2 コールバック処理

認可サーバーで利用者が同意すると、GoアプリケーションのコールバックURLへ認可コードが戻ります。ここで、状態値の一致確認を行ってから、認可コードをトークンへ交換します。

認可コードは短命で一度しか使えない前提です。認可コードをログに出したり、複数回交換しようとしたりする実装は避けます。

3.3 トークン交換

トークン交換では、Goアプリケーションが認可サーバーのトークン端点へ要求し、アクセストークンを取得します。golang.org/x/oauth2 を使うと、Exchange でこの処理を扱えます。

取得したトークンは、用途に応じてサーバー側へ安全に保存します。WebセッションIDだけをCookieへ入れ、実際のトークンはサーバー側に置く設計が安全です。

3.4 失敗時の処理

認可フローでは、利用者が同意しない、認可コードが無効、状態値が違う、トークン交換が失敗するなどのケースがあります。すべてを同じエラーにせず、監査しやすい形で記録します。

ただし、利用者へ詳細すぎる内部情報を返す必要はありません。外部表示は簡潔にし、内部ログには原因を安全に残します。

3.5 認可コードフロー例

以下は、状態値をセッションに保存する前提の簡略例です。実務では、PKCE、Cookie属性、CSRF対策、エラーハンドリングをさらに強化します。

Go実装例

package main

import (
    "context"
    "crypto/rand"
    "encoding/base64"
    "net/http"

    "golang.org/x/oauth2"
)

var config = oauthConfig()

func randomState() string {
    b := make([]byte, 32)
    _, _ = rand.Read(b)
    return base64.RawURLEncoding.EncodeToString(b)
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
    state := randomState()

    http.SetCookie(w, &http.Cookie{
        Name:     "oauth_state",
        Value:    state,
        Path:     "/",
        HttpOnly: true,
        Secure:   true,
        SameSite: http.SameSiteLaxMode,
    })

    url := config.AuthCodeURL(state, oauth2.AccessTypeOffline)
    http.Redirect(w, r, url, http.StatusFound)
}

func callbackHandler(w http.ResponseWriter, r *http.Request) {
    stateCookie, err := r.Cookie("oauth_state")
    if err != nil || stateCookie.Value != r.URL.Query().Get("state") {
        http.Error(w, "invalid state", http.StatusBadRequest)
        return
    }

    code := r.URL.Query().Get("code")
    if code == "" {
        http.Error(w, "missing code", http.StatusBadRequest)
        return
    }

    token, err := config.Exchange(context.Background(), code)
    if err != nil {
        http.Error(w, "token exchange failed", http.StatusBadGateway)
        return
    }

    _ = token
    w.WriteHeader(http.StatusOK)
    _, _ = w.Write([]byte("login completed"))
}

4. PKCE

PKCEは、認可コード横取りへの対策として使われる仕組みです。特に公開クライアントやネイティブアプリで重要ですが、現在の安全な設計ではWebアプリでも採用が推奨される場面が増えています。

RFC 7636は、公開クライアントが同じクライアントIDを共有し、アプリ内に埋め込まれた秘密値を機密として扱えない問題を前提に、Proof Key for Code Exchangeを定義しています。

4.1 PKCEが必要な理由

認可コードフローでは、認可コードが一時的にブラウザ経由で戻ってきます。もし認可コードが横取りされると、攻撃者がトークンへ交換できる可能性があります。

PKCEでは、最初に生成した検証値をもとにチャレンジを作り、トークン交換時に元の検証値を提出します。これにより、認可要求を開始したクライアントだけがコードを交換できるようにします。

4.2 code verifier

code verifierは、クライアントがランダムに生成する高エントロピー文字列です。これは外部に漏らさず、認可開始からトークン交換まで保持します。

Goでは、暗号学的に安全な乱数を使って生成します。単純な時刻や連番を使うと、推測される危険があります。

4.3 code challenge

code challengeは、code verifierから作られる値です。一般的にはSHA-256を使うS256方式を使います。平文方式は互換性目的で残っていても、実務ではS256を選ぶべきです。

認可URLにはcode challengeを送り、トークン交換時にはcode verifierを送ります。認可サーバーは両者が対応するか確認します。

4.4 OAuth 2.1との関係

OAuth 2.1の情報ページでは、認可コードフローを使うすべてのOAuthクライアントでPKCEが必須になること、リダイレクトURIの厳密な比較が必要になることなどが主要な差分として説明されています。

既存のOAuth 2.0実装でも、今から新しくGoアプリケーションを作るなら、PKCEを前提にした設計にしておくと安全です。将来の仕様やプロバイダー要件にも合わせやすくなります。

4.5 PKCE実装例

以下は、Goでcode verifierとcode challengeを作る例です。セッションにはcode verifierを保存し、認可URLにはcode challengeを含めます。

PKCE生成例

package main

import (
    "crypto/rand"
    "crypto/sha256"
    "encoding/base64"
)

func generateCodeVerifier() string {
    b := make([]byte, 64)
    _, _ = rand.Read(b)
    return base64.RawURLEncoding.EncodeToString(b)
}

func generateCodeChallenge(verifier string) string {
    sum := sha256.Sum256([]byte(verifier))
    return base64.RawURLEncoding.EncodeToString(sum[:])
}

認可URLへの追加例

func loginWithPKCEHandler(w http.ResponseWriter, r *http.Request) {
    state := randomState()
    verifier := generateCodeVerifier()
    challenge := generateCodeChallenge(verifier)

    http.SetCookie(w, &http.Cookie{
        Name:     "pkce_verifier",
        Value:    verifier,
        Path:     "/",
        HttpOnly: true,
        Secure:   true,
        SameSite: http.SameSiteLaxMode,
    })

    url := config.AuthCodeURL(
        state,
        oauth2.SetAuthURLParam("code_challenge", challenge),
        oauth2.SetAuthURLParam("code_challenge_method", "S256"),
    )

    http.Redirect(w, r, url, http.StatusFound)
}

5. トークン管理

OAuth 2.0実装では、トークン管理が安全性を大きく左右します。アクセストークン、リフレッシュトークン、IDトークンは役割が違い、保存場所、期限、失効方法、ログ出力の扱いも変える必要があります。

Go言語では、構造体としてトークンを扱うのは簡単ですが、安全な保管、暗号化、期限管理、更新、削除まで設計しなければ実務では不十分です。

5.1 アクセストークン

アクセストークンは、APIへアクセスするための短命な資格情報です。Bearer Tokenとして使う場合、保持しているだけで利用できるため、漏えいの影響が大きくなります。RFC 6750はBearer Tokenの性質として、所有者が暗号鍵を証明せずに利用できる点を説明しています。

Goアプリケーションでは、アクセストークンをログに出さず、URLクエリにも入れず、HTTPヘッダーで送ります。保存が必要な場合は、暗号化や短い保存期間を検討します。

5.2 リフレッシュトークン

リフレッシュトークンは、新しいアクセストークンを取得するために使う長寿命の資格情報です。アクセストークンよりも漏えい時の影響が大きいため、より厳格に保護します。

サーバー側で暗号化して保存し、利用者がログアウトしたときや連携解除したときに削除します。認可サーバー側でローテーションや失効が使える場合は活用します。

5.3 有効期限

トークンには有効期限があります。Goアプリケーションでは、有効期限を見て必要に応じて更新する処理を組み込みます。

期限切れのたびに利用者へ再ログインを求めると使い勝手が悪くなります。一方で、長すぎる期限は漏えい時の危険を増やすため、用途に応じてバランスを取ります。

5.4 トークン保存

Webアプリケーションでは、トークンをブラウザに保存するより、サーバー側で保存し、ブラウザにはセッションIDだけを持たせる構成が安全です。特にリフレッシュトークンはブラウザから遠ざけます。

Goでは、データベースや秘密情報保管先にトークンを暗号化して保存できます。暗号鍵の管理も必要になるため、保存処理と鍵管理を分けて考えます。

5.5 トークン保存例

以下は、トークンを保存する前に暗号化するための構造例です。実務では鍵管理サービスやローテーションも含めて設計します。

保存用構造例

package tokenstore

import "time"

type StoredOAuthToken struct {
    UserID                string
    Provider             string
    EncryptedAccessToken  []byte
    EncryptedRefreshToken []byte
    Expiry                time.Time
    Scope                 string
    CreatedAt             time.Time
    UpdatedAt             time.Time
}

type Store interface {
    Save(token StoredOAuthToken) error
    Find(userID string, provider string) (*StoredOAuthToken, error)
    Delete(userID string, provider string) error
}

6. 保護API

GoでAPIを作る場合、OAuth 2.0のアクセストークンを受け取り、保護された資源へのアクセスを制御する資源サーバーとして動くことがあります。この場合、単にAuthorizationヘッダーがあるかを見るだけでは不十分です。

資源サーバーは、トークンが本物か、有効期限内か、意図した発行者から来たか、このAPI向けか、必要な権限範囲を持つかを確認します。

6.1 Authorizationヘッダー

Bearer Tokenは通常、Authorization: Bearer <token> というHTTPヘッダーで送られます。RFC 6750はBearer Tokenの利用方法を定義する仕様です。

Goのミドルウェアでは、このヘッダーを取り出し、形式を確認してから検証処理へ渡します。形式が不正な場合は、早めに401応答を返します。

6.2 JWT検証

アクセストークンがJWTの場合、署名検証が必要です。署名を検証せずに中身だけ読む実装は危険です。

さらに、expissaudscope などの確認も必要です。トークンの形が正しくても、別API向けに発行されたトークンを受け入れてはいけません。

6.3 不透明トークン

不透明トークンは、資源サーバー側では中身が分からない文字列です。この場合、認可サーバーの検査端点へ問い合わせて有効性や権限を確認する設計になります。

不透明トークンは、トークンの意味を認可サーバー側に閉じ込めやすい利点があります。一方で、検査端点への通信遅延や可用性を考慮する必要があります。

6.4 権限範囲

OAuth 2.0では、権限範囲を使って許可された操作を限定できます。たとえば、orders:readorders:write を分ければ、読み取り専用トークンで作成処理を実行できないようにできます。

GoのAPIでは、ハンドラーごとに必要な権限範囲を明示します。認可判定を各ハンドラーへ散らばらせすぎると保守しにくいため、共通ミドルウェア化します。

6.5 Bearer検証ミドルウェア例

以下は、Authorizationヘッダーを取り出す簡略ミドルウェアです。実務では、ここからJWT検証や検査端点問い合わせへ接続します。

Goミドルウェア例

 

package auth

import (
    "context"
    "net/http"
    "strings"
)

type contextKey string

const tokenKey contextKey = "access_token"

func BearerTokenMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        header := r.Header.Get("Authorization")
        if header == "" {
            http.Error(w, "missing authorization header", http.StatusUnauthorized)
            return
        }

        const prefix = "Bearer "
        if !strings.HasPrefix(header, prefix) {
            http.Error(w, "invalid authorization scheme", http.StatusUnauthorized)
            return
        }

        token := strings.TrimSpace(strings.TrimPrefix(header, prefix))
        if token == "" {
            http.Error(w, "empty token", http.StatusUnauthorized)
            return
        }

        ctx := context.WithValue(r.Context(), tokenKey, token)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

 

7. OAuth 2.0とOpenID Connectの違い

OAuth 2.0とOpenID Connectは混同されやすいですが、目的が違います。OAuth 2.0は認可のための枠組みであり、OpenID ConnectはOAuth 2.0の上で利用者の認証結果を扱う識別層です。

ログインを実装したいGoアプリケーションでは、OAuth 2.0だけで「ログイン済み」と判断しない方が安全です。本人確認にはIDトークンを検証するOpenID Connectの流れを使います。

項目OAuth 2.0OpenID Connect
主な目的APIへの認可利用者認証
中心となるトークンアクセストークンIDトークン
利用者情報標準の本人確認情報ではない標準化されたクレームを扱う
Goでの用途外部API呼び出し、API保護ログイン、本人確認
注意点認証として使わないIDトークン検証が必要
代表的な確認scope, aud, expiss, aud, exp, nonce

7.1 OAuth 2.0の目的

OAuth 2.0の目的は、保護された資源へのアクセスを許可することです。たとえば、利用者の同意を得て外部APIへアクセスする処理が典型です。

OAuth 2.0だけでは、利用者が誰であるかを標準的に示すIDトークンは定義されません。アクセストークンの中身をログイン情報として勝手に使う設計は避けます。

7.2 OpenID Connectの目的

OpenID Connectは、OAuth 2.0の上に作られた識別層です。OpenID Connect Core 1.0は、認可サーバーによる認証に基づいて、クライアントが利用者の本人性を検証し、基本プロフィール情報を取得できる仕組みとして説明されています。

GoのWebアプリで「Googleでログイン」「社内IDでログイン」を安全に扱う場合、OpenID ConnectのIDトークン検証を行う設計が適しています。

7.3 IDトークン

IDトークンは、利用者の認証結果を表すトークンです。アクセストークンとは用途が違い、APIアクセスではなく、クライアントが利用者を識別するために使います。

IDトークンも署名、有効期限、発行者、対象者、nonceなどを検証する必要があります。単にデコードしてメールアドレスを読むだけでは安全ではありません。

7.4 選び方

Goアプリケーションが「外部APIを呼びたい」ならOAuth 2.0を使います。Goアプリケーションが「利用者をログインさせたい」ならOpenID Connectを使います。

多くのログイン連携では、OpenID Connectの認証結果とOAuth 2.0のアクセストークンが同時に使われます。目的ごとにトークンを分けて扱うことが安全です。

8. Go Webアプリでのログイン連携

GoのWebアプリでログイン連携を実装する場合、単に認可サーバーから戻ってきたらログイン完了にするのではなく、状態値、セッション、IDトークン検証、利用者作成、ログアウトまで設計します。

OAuth 2.0だけを使ってログイン風の実装をすることはできますが、本人確認を厳密にしたい場合はOpenID Connectを使う方が適切です。

8.1 ログイン開始

ログイン開始では、状態値と必要に応じてnonceを生成し、セッションへ保存します。認可URLへ利用者をリダイレクトし、認可サーバー側でログインと同意を行います。

状態値はCSRF対策として重要です。nonceはOpenID ConnectでIDトークンと認可要求を結び付けるために使います。

8.2 コールバック受信

コールバック受信では、状態値の照合を最初に行います。照合に失敗した場合、認可コードの交換を行ってはいけません。

状態値が正しい場合だけ、認可コードをトークンへ交換します。交換後、IDトークンがある場合は署名とクレームを検証します。

8.3 利用者紐付け

外部プロバイダーから取得した利用者識別子を、自分のアプリケーション内の利用者と紐付けます。メールアドレスだけを主キーにすると、プロバイダー変更や未確認メールで問題が起きることがあります。

識別には、発行者とサブジェクト識別子の組み合わせを使う設計が安全です。Goのデータベース設計でも、外部ID連携テーブルを分けると保守しやすくなります。

8.4 セッション発行

OAuth 2.0やOpenID Connectの処理が成功したら、自分のアプリケーション用のセッションを発行します。以降の画面遷移では、このアプリケーションセッションを使います。

アクセストークンをそのままログインセッションとして使うのは避けます。用途が違うため、アプリケーションのログイン状態とAPIアクセス権限は分けて管理します。

8.5 ログイン完了例

以下は、認可後にアプリケーションセッションを発行する考え方の例です。IDトークン検証部分は利用するOpenID Connectライブラリやプロバイダーに合わせて実装します。

セッション発行例

package main

import (
    "crypto/rand"
    "encoding/base64"
    "net/http"
    "time"
)

func newSessionID() string {
    b := make([]byte, 32)
    _, _ = rand.Read(b)
    return base64.RawURLEncoding.EncodeToString(b)
}

func issueAppSession(w http.ResponseWriter, userID string) {
    sessionID := newSessionID()

    // 実務では sessionID と userID をサーバー側ストアへ保存する。
    _ = userID

    http.SetCookie(w, &http.Cookie{
        Name:     "app_session",
        Value:    sessionID,
        Path:     "/",
        HttpOnly: true,
        Secure:   true,
        SameSite: http.SameSiteLaxMode,
        Expires:  time.Now().Add(24 * time.Hour),
    })
}

9. 外部APIクライアント

Goアプリケーションが外部APIを呼び出す場合、OAuth 2.0クライアントとしてアクセストークンを取得し、認可済みHTTPクライアントで要求を送ります。golang.org/x/oauth2 は、OAuth 2.0認可済みHTTP要求を作るための支援を提供しています。

外部API連携では、トークン更新、権限範囲、エラー処理、レート制限、再試行を合わせて設計する必要があります。トークンを取得できるだけでは、本番運用には足りません。

9.1 認可済みHTTPクライアント

oauth2.Config から取得したトークンを使うと、認可済みHTTPクライアントを作れます。このクライアントは、要求にアクセストークンを付ける処理を簡単に扱えます。

ただし、どのトークンを誰の代わりに使っているかはアプリケーション側で管理する必要があります。利用者ごとのトークンと、サービス全体のトークンを混同しないようにします。

9.2 権限範囲

外部APIへ要求する権限範囲は、必要最小限にします。読み取りだけで十分なら、書き込み権限を要求しない方が安全です。

利用者に表示する同意画面でも、過剰な権限要求は不信感につながります。Goの設定では、スコープを用途ごとに整理しておくと保守しやすくなります。

9.3 エラー処理

外部APIは、401、403、429、500などさまざまなエラーを返します。401はトークン期限切れ、403は権限不足、429はレート制限の可能性があります。

GoのHTTPクライアントでは、応答コードごとに処理を分けます。すべてを単純な再試行にすると、外部APIへ不要な負荷をかける危険があります。

9.4 再試行

外部API呼び出しでは、一時的なネットワーク障害やサーバーエラーに備えて再試行を設計します。ただし、認可エラーや権限不足を再試行しても改善しません。

再試行には、回数制限、待機時間、指数バックオフ、文脈キャンセルを入れます。Goではcontext.Contextを使い、呼び出し元のタイムアウトを下流へ伝える設計が重要です。

9.5 外部API呼び出し例

以下は、OAuth 2.0トークンを使って外部APIを呼ぶ簡略例です。実務では、応答コードごとの処理と監視を追加します。

Goクライアント例

package external

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"

    "golang.org/x/oauth2"
)

type Profile struct {
    ID    string `json:"id"`
    Email string `json:"email"`
    Name  string `json:"name"`
}

func FetchProfile(ctx context.Context, token *oauth2.Token, endpoint string) (*Profile, error) {
    client := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token))

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
    if err != nil {
        return nil, err
    }

    res, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer res.Body.Close()

    if res.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("profile api failed: status=%d", res.StatusCode)
    }

    var profile Profile
    if err := json.NewDecoder(res.Body).Decode(&profile); err != nil {
        return nil, err
    }

    return &profile, nil
}

10. サービス間認可

マイクロサービス構成では、利用者が直接操作するWebアプリだけでなく、サービス同士がAPIを呼び合う場面があります。この場合もOAuth 2.0の考え方を使って、呼び出し元サービスに限定的な権限を与えられます。

サービス間認可では、利用者代理のトークンと、サービス自身のトークンを分けることが重要です。注文サービスが決済サービスを呼ぶとき、その呼び出しが利用者代理なのか、システム処理なのかで扱う権限が変わります。

10.1 クライアント資格情報フロー

サービス間通信では、クライアント資格情報フローが使われることがあります。この方式では、利用者ではなくクライアント自身の資格情報でアクセストークンを取得します。

ただし、これは利用者の同意を表すものではありません。システム間の権限として扱い、サービスごとに必要最小限の範囲を与えます。

10.2 利用者代理

利用者が行った操作を下流サービスへ伝える場合、利用者代理の文脈が必要になることがあります。単に上流サービスの資格情報で下流を呼ぶと、誰の操作か分からなくなります。

この設計では、トークン交換、委任、内部トークンなどの設計を慎重に行います。監査ログには、呼び出し元サービスと利用者の両方を記録できるようにします。

10.3 内部APIの保護

内部APIだからといって認可を省略するのは危険です。ネットワーク内部に侵入された場合、認可なしの内部APIは攻撃者に使われやすくなります。

Goの内部APIでも、Bearer Token検証、mTLS、サービス識別、権限範囲を組み合わせて保護します。特に管理系APIや個人情報を扱うAPIは厳格にします。

10.4 権限範囲設計

サービス間の権限範囲は、人間向けの画面権限とは別に設計します。たとえば、payment:createinventory:reservenotification:send のように操作単位で分けます。

範囲が粗すぎると過剰権限になり、細かすぎると運用が複雑になります。重要な資源と危険な操作を中心に分けるのが現実的です。

10.5 クライアント資格情報例

以下は、Goでクライアント資格情報を使ってトークンを取得する例です。clientcredentialsgolang.org/x/oauth2 の関連パッケージとして使われます。

Go実装例

package serviceauth

import (
    "context"
    "net/http"

    "golang.org/x/oauth2/clientcredentials"
)

func NewServiceClient(ctx context.Context) *http.Client {
    conf := clientcredentials.Config{
        ClientID:     "order-service",
        ClientSecret: "replace-with-secret-from-vault",
        TokenURL:     "https://auth.example.com/oauth/token",
        Scopes: []string{
            "payment:create",
        },
    }

    return conf.Client(ctx)
}

11. リフレッシュトークン運用

リフレッシュトークンは、OAuth 2.0実装の中でも特に慎重に扱うべき要素です。アクセストークンが期限切れになった後も、新しいアクセストークンを取得できるため、漏えい時の影響が長く続きます。

Goアプリケーションでは、リフレッシュトークンを保存する場合、暗号化、アクセス制御、失効、ローテーション、監査ログを組み合わせます。

11.1 保存場所

リフレッシュトークンは、ブラウザではなくサーバー側に保存する構成が安全です。保存先はデータベースでもよいですが、値自体は暗号化します。

暗号鍵はアプリケーションコードや環境変数に固定せず、可能であれば鍵管理サービスを使います。保存先と鍵の両方が漏れなければ復号できない構成を目指します。

11.2 ローテーション

認可サーバーがリフレッシュトークンローテーションを提供している場合、更新のたびに新しいリフレッシュトークンへ差し替えます。古いトークンが再利用された場合は、不正利用の兆候として扱えます。

Goの保存処理では、トークン更新と保存を原子的に行うことが重要です。途中で失敗すると、古いトークンも新しいトークンも使えなくなる場合があります。

11.3 失効

利用者が連携解除した場合、ログアウトした場合、権限が変わった場合、端末紛失が疑われる場合は、リフレッシュトークンを失効させます。自分の保存先から削除するだけでなく、認可サーバー側の失効端点があれば呼び出します。

失効処理は監査ログに残します。誰が、いつ、どのプロバイダー連携を解除したかを追えるようにします。

11.4 更新失敗

リフレッシュトークンによる更新が失敗することはあります。トークン失効、権限取り消し、プロバイダー側の変更、利用者のパスワード変更などが原因になります。

Goアプリケーションでは、更新失敗を無限に再試行しないようにします。必要なら利用者に再認可を求めます。

11.5 更新処理例

以下は、期限切れ時にトークンを更新する考え方の例です。実務では保存処理、排他制御、失効時の再認可導線を追加します。

更新処理例

package tokenrefresh

import (
    "context"
    "time"

    "golang.org/x/oauth2"
)

func RefreshIfNeeded(ctx context.Context, conf *oauth2.Config, token *oauth2.Token) (*oauth2.Token, error) {
    if token.Valid() && time.Until(token.Expiry) > time.Minute {
        return token, nil
    }

    source := conf.TokenSource(ctx, token)
    newToken, err := source.Token()
    if err != nil {
        return nil, err
    }

    return newToken, nil
}

12. 安全な実装

OAuth 2.0は仕様どおりに見えても、細部の実装ミスで脆弱になります。Go言語では堅牢な標準ライブラリを使えますが、状態値、Cookie属性、トークン検証、秘密値管理、リダイレクトURIなどは開発者が正しく設計する必要があります。

RFC 9700は、OAuth 2.0の現在の安全な実装慣行をまとめ、既存仕様の脅威モデルと安全助言を更新・拡張する文書です。新規実装では、古いサンプルだけではなく現在の推奨事項も確認すべきです。

12.1 リダイレクトURI

リダイレクトURIは厳密に比較します。部分一致や前方一致にすると、攻撃者のURLへ認可コードが送られる可能性があります。

OAuth 2.1の説明でも、リダイレクトURIの比較が主要な安全上の差分として挙げられています。Go実装では、設定値として登録済みURIだけを使い、動的に任意URLを受け取らないようにします。

12.2 state

stateは、認可要求とコールバックを結び付けるために使います。これがないと、攻撃者が自分の認可結果を被害者のセッションへ結び付けるような攻撃が起こり得ます。

Goでは、暗号学的乱数でstateを生成し、HTTP専用Cookieやサーバー側セッションに保存します。コールバックでは必ず一致確認を行います。

12.3 PKCE

PKCEは、認可コード横取りに対する重要な防御です。RFC 7636は、公開クライアントではクライアント秘密値を機密として扱えない問題を前提にPKCEを定義しています。

現在の設計では、公開クライアントだけでなく、WebアプリでもPKCEを使う方向が安全です。Go実装でも認可URL生成時にcode challengeを追加し、交換時にcode verifierを送ります。

12.4 秘密値管理

クライアント秘密値、暗号鍵、トークンはリポジトリに入れてはいけません。コンテナ画像に焼き込むことも避けます。

Goアプリケーションでは、起動時に秘密情報管理サービスや安全な環境変数から読み込みます。ログへ出さないことも重要です。

12.5 安全設定例

以下は、OAuth 2.0ログインで使うCookieの安全属性例です。ローカル開発と本番ではSecure属性の扱いが変わるため、環境ごとに確認します。

Cookie設定例

func setSecureCookie(w http.ResponseWriter, name string, value string, maxAge int) {
    http.SetCookie(w, &http.Cookie{
        Name:     name,
        Value:    value,
        Path:     "/",
        MaxAge:   maxAge,
        HttpOnly: true,
        Secure:   true,
        SameSite: http.SameSiteLaxMode,
    })
}

13. テスト

OAuth 2.0実装は外部サービスに依存しやすいため、テスト設計が重要です。毎回本物の認可サーバーを使うと、テストが遅く、不安定になり、秘密情報も必要になります。

Goでは、HTTPテストサーバー、モック、インターフェース、テスト用トークンを使って、認可URL生成、コールバック処理、トークン検証、API保護を段階的に確認できます。

13.1 認可URLテスト

認可URLテストでは、必要なパラメータが含まれているかを確認します。クライアントID、リダイレクトURI、状態値、権限範囲、PKCEパラメータが対象です。

URL生成は小さな処理ですが、設定ミスがあるとログイン全体が壊れます。Goの単体テストで早めに検出できるようにします。

13.2 コールバックテスト

コールバックテストでは、状態値が一致した場合だけトークン交換へ進むことを確認します。状態値が違う、認可コードがない、認可サーバーがエラーを返すケースも試験します。

OAuth 2.0では失敗系の処理が安全性に直結します。正常系だけのテストでは不十分です。

13.3 トークン検証テスト

API保護では、トークンがない、不正形式、期限切れ、権限不足、発行者違い、対象者違いをテストします。JWTを使う場合は、署名検証の失敗も必ず確認します。

Goのミドルウェアは、テストしやすいように検証処理をインターフェース化すると便利です。実装とテスト用の検証器を差し替えられます。

13.4 外部APIモック

外部API呼び出しは、httptest.Server を使ってモックできます。トークン付き要求が正しく送られているか、エラー時の処理が正しいかを確認します。

本物の外部APIを単体テストで呼び出すと、ネットワークやレート制限の影響を受けます。結合テストと単体テストを分けることが重要です。

13.5 ミドルウェアテスト例

以下は、Bearer Tokenミドルウェアの簡単なテスト例です。実務では、検証器を差し替えて権限範囲まで確認します。

Goテスト例

package auth_test

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "example.com/app/auth"
)

func TestBearerTokenMiddlewareRejectsMissingHeader(t *testing.T) {
    next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    })

    handler := auth.BearerTokenMiddleware(next)

    req := httptest.NewRequest(http.MethodGet, "/orders", nil)
    res := httptest.NewRecorder()

    handler.ServeHTTP(res, req)

    if res.Code != http.StatusUnauthorized {
        t.Fatalf("expected 401, got %d", res.Code)
    }
}

func TestBearerTokenMiddlewareAcceptsBearerHeader(t *testing.T) {
    next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    })

    handler := auth.BearerTokenMiddleware(next)

    req := httptest.NewRequest(http.MethodGet, "/orders", nil)
    req.Header.Set("Authorization", "Bearer test-token")
    res := httptest.NewRecorder()

    handler.ServeHTTP(res, req)

    if res.Code != http.StatusOK {
        t.Fatalf("expected 200, got %d", res.Code)
    }
}

14. 監視と運用

OAuth 2.0実装は、作って終わりではありません。トークン交換失敗、更新失敗、認可拒否、権限不足、プロバイダー障害、不正なコールバックなどを監視する必要があります。

Goアプリケーションでは、ログ、メトリクス、監査イベントを分けて設計します。トークン値や個人情報を出さずに、運用判断に必要な情報を残します。

14.1 ログ

OAuth 2.0関連ログには、処理種別、結果、プロバイダー、利用者ID、エラー種別、要求IDを含めます。ただし、アクセストークン、リフレッシュトークン、認可コードは出してはいけません。

ログは障害調査に役立ちますが、秘密情報漏えいの経路にもなります。安全なマスキングを徹底します。

14.2 メトリクス

メトリクスでは、ログイン開始数、コールバック成功数、トークン交換失敗数、更新失敗数、API認可失敗数を測ります。急な失敗増加はプロバイダー障害や設定ミスの兆候になります。

Goでは、Prometheusなどと組み合わせてカウンターやヒストグラムを出せます。OAuth処理の遅延も測ると、認可サーバー側の遅延に気づきやすくなります。

14.3 監査

監査ログには、連携開始、連携解除、権限変更、トークン失効、管理者操作を記録します。利用者のセキュリティ問い合わせや内部監査に必要です。

監査ログは通常ログより改ざんに強く、保存期間も明確にします。Goアプリケーションからは専用の監査出力へ送る設計が望ましいです。

14.4 アラート

トークン交換失敗率が急増した場合、ログインができない状態かもしれません。API認可失敗が急増した場合、クライアント設定や鍵更新の問題かもしれません。

アラートは、単発の失敗ではなく割合や継続時間で設定します。誤報が多いと運用チームが反応しなくなるため、重要度を分けます。

14.5 メトリクス例

以下は、OAuth処理の結果を記録するための簡略例です。実務では利用する監視基盤に合わせて実装します。

計測例

package metrics

import "log"

func RecordOAuthEvent(provider string, event string, success bool) {
    log.Printf(
        "metric=oauth_event provider=%s event=%s success=%t",
        provider,
        event,
        success,
    )
}

 

 

15. 実践構成

Go言語でOAuth 2.0を安全に導入するには、認可フローだけでなく、設定、セッション、トークン保存、API保護、テスト、監視を一体で設計します。個別のコード片が正しくても、全体のつながりが弱いと事故につながります。

ここでは、実務で使いやすい構成を整理します。外部プロバイダーを使うWebアプリ、保護API、サービス間通信の三つを分けると設計しやすくなります。

15.1 ディレクトリ構成

OAuth 2.0関連コードは、ハンドラー、トークン保存、プロバイダー設定、ミドルウェア、テストを分けます。すべてをmain.goへ書くと、後から変更しにくくなります。

Goでは小さなパッケージに分けることで、単体テストを書きやすくなります。認可サーバーとの通信部分もインターフェース化すると、テストで差し替えやすくなります。

15.2 設定読み込み

設定は環境変数や設定ファイルから読み込みます。クライアント秘密値は秘密情報管理サービスを使い、ローカル開発用と本番用を明確に分けます。

リダイレクトURIは環境ごとに固定します。利用者入力からリダイレクト先を組み立てる設計は避けます。

15.3 パッケージ分割

auth/oauth2clientauth/sessionauth/tokenstoremiddleware のように分けると責務が明確になります。外部プロバイダー依存のコードを閉じ込めると、将来の変更に強くなります。

API保護とログイン連携も分けるべきです。ログイン処理はWebセッション、API保護はBearer Token検証というように、目的が違います。

15.4 移行手順

既存の独自ログインからOAuth 2.0やOpenID Connectへ移行する場合、いきなり全利用者を切り替えない方が安全です。まず管理者や一部利用者から始め、監視しながら拡大します。

既存セッション、既存利用者ID、外部IDの紐付け、ログアウト、連携解除を丁寧に設計します。移行時は問い合わせが増えるため、監査ログも重要になります。

15.5 全体構成例

以下は、Go WebアプリでOAuth 2.0ログインと保護APIを分けて扱う構成例です。実際にはOpenID ConnectのIDトークン検証やJWT検証を追加します。

ディレクトリ例

.
├── cmd
│   └── web
│       └── main.go
├── internal
│   ├── auth
│   │   ├── oauth_config.go
│   │   ├── login_handler.go
│   │   ├── callback_handler.go
│   │   ├── token_store.go
│   │   └── session.go
│   ├── middleware
│   │   └── bearer.go
│   ├── user
│   │   └── repository.go
│   └── api
│       └── order_handler.go
└── go.mod

 

 

ルーティング例

package main

import (
    "net/http"

    "example.com/app/internal/auth"
    "example.com/app/internal/middleware"
)

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("/login", auth.LoginHandler)
    mux.HandleFunc("/oauth/callback", auth.CallbackHandler)

    protected := middleware.BearerTokenMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte(`{"message":"protected resource"}`))
    }))

    mux.Handle("/api/orders", protected)

    _ = http.ListenAndServe(":8080", mux)
}

おわりに

Go言語でOAuth 2.0を実装するときは、認可コードをトークンへ交換する処理だけで満足してはいけません。状態値、PKCE、リダイレクトURI検証、トークン保存、リフレッシュトークン管理、API保護、ログ、監査、テストまで含めて初めて安全な実装になります。

OAuth 2.0は認可の仕組みであり、ログインや本人確認を標準的に扱いたい場合はOpenID Connectを使います。OpenID ConnectはOAuth 2.0の上に作られた識別層であり、IDトークンを検証して利用者の本人性を扱うための仕組みです。

新しくGoアプリケーションを作るなら、認可コードフロー、PKCE、厳密なリダイレクトURI、短命アクセストークン、安全なリフレッシュトークン保存を前提に設計するのが現実的です。golang.org/x/oauth2 のような実績あるパッケージを使いながら、現在のOAuth 2.0安全慣行も確認し、長く運用できる認可基盤を作ることが重要です。

 

LINE Chat