go-oauth2
認証ライブラリ
go-oauth2
概要
go-oauth2は、Go言語で本格的なOAuth 2.0認可サーバーを構築するための包括的なライブラリです。RFC 6749に完全準拠したOAuth 2.0の実装を提供し、認可コードグラント、クライアント認証、リフレッシュトークンなどの主要な機能をサポートしています。シンプルで直感的なAPIでありながら、エンタープライズレベルのOAuth 2.0プロバイダーを構築できる柔軟性と拡張性を備えており、カスタムトークンストア、認証ストア、クライアントストアの実装が可能です。JWT、カスタムトークン形式、複数のグラントタイプに対応し、2025年現在も活発に開発が続けられているGo言語のOAuth 2.0エコシステムにおける主要なライブラリの一つです。
詳細
go-oauth2は、OAuth 2.0認可サーバーを実装するためのGo言語ライブラリで、OAuth 2.0仕様の完全な実装を提供します。以下の主な特徴があります:
- OAuth 2.0準拠: RFC 6749、RFC 6750(Bearer Token)に完全準拠した実装
- グラントタイプサポート: Authorization Code、Client Credentials、Password、Refresh Token、Implicit
- 柔軟なストレージ: メモリ、Redis、MongoDB、PostgreSQLなど多様なストレージバックエンド対応
- JWT対応: JWTアクセストークンの生成・検証機能を標準サポート
- カスタマイズ性: トークン生成、クライアント認証、ユーザー認証の完全なカスタマイズが可能
- セキュリティ機能: PKCE、State、Scope、Token Introspectionなどの高度なセキュリティ機能
メリット・デメリット
メリット
- OAuth 2.0標準への完全準拠により高い互換性と信頼性を実現
- 豊富なグラントタイプとセキュリティ機能で幅広いユースケースに対応
- 柔軟なストレージ抽象化により様々なデータベースとキャッシュシステムを利用可能
- JWT標準対応でステートレスなトークン管理が可能
- シンプルなAPIでありながら高度なカスタマイズ性を提供
- 活発な開発コミュニティと豊富なドキュメント・サンプルコード
デメリット
- OAuth 2.0の複雑性により初期学習コストが高い
- 本格的な認可サーバー構築には追加的なセキュリティ実装が必要
- Go以外の言語環境では使用できない
- 単純な認証要件には過剰な機能セットとなる可能性
- エンタープライズレベルの運用には詳細な設定と監視が必要
参考ページ
書き方の例
基本的なOAuth 2.0サーバーのセットアップ
package main
import (
"log"
"net/http"
"time"
"github.com/go-oauth2/oauth2/v4/errors"
"github.com/go-oauth2/oauth2/v4/manage"
"github.com/go-oauth2/oauth2/v4/models"
"github.com/go-oauth2/oauth2/v4/server"
"github.com/go-oauth2/oauth2/v4/store"
"github.com/go-oauth2/oauth2/v4/generate"
)
func main() {
// マネージャーの作成
manager := manage.NewDefaultManager()
// トークン設定
manager.SetAuthorizeCodeTokenCfg(&manage.Config{
AccessTokenExp: time.Hour * 2,
RefreshTokenExp: time.Hour * 24 * 7,
IsGenerateRefresh: true,
})
// JWTアクセストークンジェネレーターの設定
manager.MapAccessGenerate(generate.NewJWTAccessGenerate("", []byte("your-secret-key"), jwt.SigningMethodHS256))
// クライアントストアの設定
clientStore := store.NewClientStore()
clientStore.Set("client-id-123", &models.Client{
ID: "client-id-123",
Secret: "client-secret-456",
Domain: "http://localhost:3000",
UserID: "app-user",
})
manager.MapClientStorage(clientStore)
// トークンストアの設定(メモリストア)
manager.MapTokenStorage(store.NewMemoryTokenStore())
// OAuth2サーバーの作成
srv := server.NewDefaultServer(manager)
srv.SetAllowGetAccessRequest(true)
srv.SetClientInfoHandler(server.ClientFormHandler)
// ユーザー認証ハンドラー
srv.SetUserAuthorizationHandler(userAuthorizeHandler)
// 認証スコープ処理
srv.SetAuthorizeScopeHandler(func(rw http.ResponseWriter, r *http.Request) (scope string, err error) {
// リクエストされたスコープを検証
requestScope := r.FormValue("scope")
validScopes := []string{"read", "write", "admin"}
for _, validScope := range validScopes {
if requestScope == validScope {
return requestScope, nil
}
}
return "read", nil // デフォルトスコープ
})
// エラーハンドラー
srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
log.Println("OAuth2 Internal Error:", err.Error())
return
})
// ルート設定
http.HandleFunc("/oauth/authorize", func(w http.ResponseWriter, r *http.Request) {
err := srv.HandleAuthorizeRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
})
http.HandleFunc("/oauth/token", func(w http.ResponseWriter, r *http.Request) {
err := srv.HandleTokenRequest(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
// 保護されたリソースエンドポイント
http.HandleFunc("/api/profile", func(w http.ResponseWriter, r *http.Request) {
token, err := srv.ValidationBearerToken(r)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(fmt.Sprintf(`{
"user_id": "%s",
"client_id": "%s",
"scope": "%s",
"expires_at": "%s"
}`, token.GetUserID(), token.GetClientID(), token.GetScope(), token.GetAccessCreateAt().Add(token.GetAccessExpiresIn()))))
})
log.Println("OAuth2 Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// ユーザー認証ハンドラー
func userAuthorizeHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) {
// セッションからユーザーIDを取得(簡単な例)
username := r.FormValue("username")
password := r.FormValue("password")
// 実際の実装では、データベースやLDAPでユーザー認証
if username == "testuser" && password == "testpass" {
return "user-123", nil
}
// 認証失敗時はログインページにリダイレクト
w.Header().Set("Location", "/login?redirect="+r.URL.RequestURI())
w.WriteHeader(http.StatusFound)
return "", errors.ErrAccessDenied
}
Redis ストレージバックエンドの実装
package main
import (
"encoding/json"
"time"
"github.com/go-redis/redis/v8"
"github.com/go-oauth2/oauth2/v4"
"github.com/go-oauth2/oauth2/v4/models"
)
// Redis トークンストア
type RedisTokenStore struct {
client *redis.Client
prefix string
}
func NewRedisTokenStore(client *redis.Client, prefix string) *RedisTokenStore {
return &RedisTokenStore{
client: client,
prefix: prefix,
}
}
// oauth2.TokenStore インターフェースの実装
func (rs *RedisTokenStore) Create(ctx context.Context, info oauth2.TokenInfo) error {
ct := time.Now()
code := info.GetCode()
access := info.GetAccess()
refresh := info.GetRefresh()
data, err := json.Marshal(info)
if err != nil {
return err
}
// 認可コードの保存
if code != "" {
key := rs.prefix + "code:" + code
expiration := info.GetCodeCreateAt().Add(info.GetCodeExpiresIn()).Sub(ct)
err = rs.client.Set(ctx, key, data, expiration).Err()
if err != nil {
return err
}
}
// アクセストークンの保存
if access != "" {
key := rs.prefix + "access:" + access
expiration := info.GetAccessCreateAt().Add(info.GetAccessExpiresIn()).Sub(ct)
err = rs.client.Set(ctx, key, data, expiration).Err()
if err != nil {
return err
}
}
// リフレッシュトークンの保存
if refresh != "" {
key := rs.prefix + "refresh:" + refresh
expiration := info.GetRefreshCreateAt().Add(info.GetRefreshExpiresIn()).Sub(ct)
err = rs.client.Set(ctx, key, data, expiration).Err()
if err != nil {
return err
}
}
return nil
}
func (rs *RedisTokenStore) GetByCode(ctx context.Context, code string) (oauth2.TokenInfo, error) {
key := rs.prefix + "code:" + code
data, err := rs.client.Get(ctx, key).Result()
if err != nil {
return nil, err
}
var token models.Token
err = json.Unmarshal([]byte(data), &token)
return &token, err
}
func (rs *RedisTokenStore) GetByAccess(ctx context.Context, access string) (oauth2.TokenInfo, error) {
key := rs.prefix + "access:" + access
data, err := rs.client.Get(ctx, key).Result()
if err != nil {
return nil, err
}
var token models.Token
err = json.Unmarshal([]byte(data), &token)
return &token, err
}
func (rs *RedisTokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2.TokenInfo, error) {
key := rs.prefix + "refresh:" + refresh
data, err := rs.client.Get(ctx, key).Result()
if err != nil {
return nil, err
}
var token models.Token
err = json.Unmarshal([]byte(data), &token)
return &token, err
}
func (rs *RedisTokenStore) RemoveByCode(ctx context.Context, code string) error {
key := rs.prefix + "code:" + code
return rs.client.Del(ctx, key).Err()
}
func (rs *RedisTokenStore) RemoveByAccess(ctx context.Context, access string) error {
key := rs.prefix + "access:" + access
return rs.client.Del(ctx, key).Err()
}
func (rs *RedisTokenStore) RemoveByRefresh(ctx context.Context, refresh string) error {
key := rs.prefix + "refresh:" + refresh
return rs.client.Del(ctx, key).Err()
}
// Redis ストアの使用例
func setupWithRedis() {
// Redis クライアントの設定
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
manager := manage.NewDefaultManager()
// Redis トークンストアを設定
tokenStore := NewRedisTokenStore(rdb, "oauth2:")
manager.MapTokenStorage(tokenStore)
// 他の設定...
}
JWT アクセストークンとカスタムクレーム
package main
import (
"context"
"encoding/json"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/go-oauth2/oauth2/v4"
"github.com/go-oauth2/oauth2/v4/generate"
)
// カスタムクレーム構造体
type CustomClaims struct {
UserID string `json:"user_id"`
ClientID string `json:"client_id"`
Scope string `json:"scope"`
Permissions []string `json:"permissions"`
Role string `json:"role"`
jwt.StandardClaims
}
// カスタムJWTジェネレーター
type CustomJWTGenerator struct {
signingKey []byte
signingMethod jwt.SigningMethod
}
func NewCustomJWTGenerator(key []byte, method jwt.SigningMethod) *CustomJWTGenerator {
return &CustomJWTGenerator{
signingKey: key,
signingMethod: method,
}
}
// oauth2.AccessGenerate インターフェースの実装
func (cjg *CustomJWTGenerator) Token(ctx context.Context, data *oauth2.GenerateBasic, isGenRefresh bool) (string, string, error) {
// カスタムクレームの構築
claims := CustomClaims{
UserID: data.UserID,
ClientID: data.Client.GetID(),
Scope: data.Request.Scope,
StandardClaims: jwt.StandardClaims{
Audience: data.Client.GetID(),
Subject: data.UserID,
Issuer: "oauth2-server",
ExpiresAt: time.Now().Add(data.TokenInfo.GetAccessExpiresIn()).Unix(),
IssuedAt: time.Now().Unix(),
NotBefore: time.Now().Unix(),
},
}
// ユーザー情報に基づく権限設定
claims.Permissions = getUserPermissions(data.UserID)
claims.Role = getUserRole(data.UserID)
// JWTトークン生成
token := jwt.NewWithClaims(cjg.signingMethod, claims)
access, err := token.SignedString(cjg.signingKey)
if err != nil {
return "", "", err
}
var refresh string
if isGenRefresh {
// リフレッシュトークンの生成
refreshClaims := jwt.StandardClaims{
Subject: data.UserID,
Issuer: "oauth2-server",
ExpiresAt: time.Now().Add(data.TokenInfo.GetRefreshExpiresIn()).Unix(),
IssuedAt: time.Now().Unix(),
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
refresh, err = refreshToken.SignedString(cjg.signingKey)
if err != nil {
return "", "", err
}
}
return access, refresh, nil
}
// ユーザー権限取得(データベースから実際に取得)
func getUserPermissions(userID string) []string {
// データベースやキャッシュからユーザー権限を取得
userPerms := map[string][]string{
"user-123": {"read:profile", "write:profile"},
"admin-456": {"read:profile", "write:profile", "admin:users", "admin:system"},
}
if perms, exists := userPerms[userID]; exists {
return perms
}
return []string{"read:profile"}
}
func getUserRole(userID string) string {
userRoles := map[string]string{
"user-123": "user",
"admin-456": "admin",
}
if role, exists := userRoles[userID]; exists {
return role
}
return "user"
}
// JWT 検証ミドルウェア
func jwtValidationMiddleware(signingKey []byte) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString := extractTokenFromHeader(r)
if tokenString == "" {
http.Error(w, "Missing authorization token", http.StatusUnauthorized)
return
}
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return signingKey, nil
})
if err != nil || !token.Valid {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
claims := token.Claims.(*CustomClaims)
// コンテキストにクレーム情報を設定
ctx := context.WithValue(r.Context(), "user_claims", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func extractTokenFromHeader(r *http.Request) string {
authHeader := r.Header.Get("Authorization")
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
return authHeader[7:]
}
return ""
}
PKCE (Proof Key for Code Exchange) 対応
package main
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"net/http"
"github.com/go-oauth2/oauth2/v4/server"
)
// PKCE チャレンジ生成
func generatePKCE() (verifier, challenge string, err error) {
// Code Verifier 生成 (43-128文字のランダム文字列)
verifierBytes := make([]byte, 32)
_, err = rand.Read(verifierBytes)
if err != nil {
return
}
verifier = base64.RawURLEncoding.EncodeToString(verifierBytes)
// Code Challenge 生成 (SHA256 ハッシュ)
hash := sha256.Sum256([]byte(verifier))
challenge = base64.RawURLEncoding.EncodeToString(hash[:])
return
}
// PKCE 検証関数
func validatePKCE(codeVerifier, codeChallenge, codeChallengeMethod string) bool {
if codeChallengeMethod == "S256" {
hash := sha256.Sum256([]byte(codeVerifier))
expected := base64.RawURLEncoding.EncodeToString(hash[:])
return expected == codeChallenge
} else if codeChallengeMethod == "plain" {
return codeVerifier == codeChallenge
}
return false
}
// PKCE 対応OAuth2サーバー設定
func setupPKCEServer() {
manager := manage.NewDefaultManager()
srv := server.NewDefaultServer(manager)
// PKCE 検証ハンドラー
srv.SetExtensionFieldsHandler(func(ti oauth2.TokenInfo) map[string]interface{} {
return map[string]interface{}{
"pkce_supported": true,
}
})
// 認可リクエスト処理でのPKCE検証
srv.SetClientScopeHandler(func(tgr *oauth2.TokenGenerateRequest) (allowed bool, err error) {
codeChallenge := tgr.Request.Form.Get("code_challenge")
codeChallengeMethod := tgr.Request.Form.Get("code_challenge_method")
// PKCE パラメータの検証
if codeChallenge != "" {
if codeChallengeMethod == "" {
codeChallengeMethod = "plain"
}
if codeChallengeMethod != "S256" && codeChallengeMethod != "plain" {
return false, errors.New("unsupported code challenge method")
}
// PKCE パラメータを保存(実際にはセッションやDBに保存)
storePKCEChallenge(tgr.ClientID, codeChallenge, codeChallengeMethod)
}
return true, nil
})
// トークン交換時のPKCE検証
srv.SetRefreshingScopeHandler(func(tgr *oauth2.TokenGenerateRequest, oldScope string) (allowed bool, err error) {
codeVerifier := tgr.Request.Form.Get("code_verifier")
if codeVerifier != "" {
// 保存されたチャレンジを取得
challenge, method := getPKCEChallenge(tgr.ClientID)
if !validatePKCE(codeVerifier, challenge, method) {
return false, errors.New("invalid code verifier")
}
// 検証成功後はチャレンジを削除
removePKCEChallenge(tgr.ClientID)
}
return true, nil
})
}
// PKCE チャレンジ保存・取得・削除(実際にはRedisやDBを使用)
var pkceStore = make(map[string]map[string]string)
func storePKCEChallenge(clientID, challenge, method string) {
if pkceStore[clientID] == nil {
pkceStore[clientID] = make(map[string]string)
}
pkceStore[clientID]["challenge"] = challenge
pkceStore[clientID]["method"] = method
}
func getPKCEChallenge(clientID string) (challenge, method string) {
if client, exists := pkceStore[clientID]; exists {
return client["challenge"], client["method"]
}
return "", ""
}
func removePKCEChallenge(clientID string) {
delete(pkceStore, clientID)
}
OAuth2 クライアント実装
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
)
// OAuth2 クライアント
type OAuth2Client struct {
ClientID string
ClientSecret string
RedirectURI string
AuthURL string
TokenURL string
Scope string
}
// 認可URL生成
func (c *OAuth2Client) GetAuthorizationURL(state string) string {
params := url.Values{}
params.Add("client_id", c.ClientID)
params.Add("redirect_uri", c.RedirectURI)
params.Add("response_type", "code")
params.Add("scope", c.Scope)
params.Add("state", state)
return c.AuthURL + "?" + params.Encode()
}
// PKCEサポート付き認可URL生成
func (c *OAuth2Client) GetAuthorizationURLWithPKCE(state string) (string, string, error) {
verifier, challenge, err := generatePKCE()
if err != nil {
return "", "", err
}
params := url.Values{}
params.Add("client_id", c.ClientID)
params.Add("redirect_uri", c.RedirectURI)
params.Add("response_type", "code")
params.Add("scope", c.Scope)
params.Add("state", state)
params.Add("code_challenge", challenge)
params.Add("code_challenge_method", "S256")
authURL := c.AuthURL + "?" + params.Encode()
return authURL, verifier, nil
}
// トークン交換
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope"`
}
func (c *OAuth2Client) ExchangeCodeForToken(code, codeVerifier string) (*TokenResponse, error) {
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("client_id", c.ClientID)
data.Set("client_secret", c.ClientSecret)
data.Set("code", code)
data.Set("redirect_uri", c.RedirectURI)
if codeVerifier != "" {
data.Set("code_verifier", codeVerifier)
}
resp, err := http.Post(c.TokenURL, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("token exchange failed: %s", resp.Status)
}
var tokenResp TokenResponse
err = json.NewDecoder(resp.Body).Decode(&tokenResp)
return &tokenResp, err
}
// リフレッシュトークンでアクセストークン更新
func (c *OAuth2Client) RefreshAccessToken(refreshToken string) (*TokenResponse, error) {
data := url.Values{}
data.Set("grant_type", "refresh_token")
data.Set("client_id", c.ClientID)
data.Set("client_secret", c.ClientSecret)
data.Set("refresh_token", refreshToken)
resp, err := http.Post(c.TokenURL, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("token refresh failed: %s", resp.Status)
}
var tokenResp TokenResponse
err = json.NewDecoder(resp.Body).Decode(&tokenResp)
return &tokenResp, err
}
// 使用例
func clientExample() {
client := &OAuth2Client{
ClientID: "client-id-123",
ClientSecret: "client-secret-456",
RedirectURI: "http://localhost:3000/callback",
AuthURL: "http://localhost:8080/oauth/authorize",
TokenURL: "http://localhost:8080/oauth/token",
Scope: "read write",
}
// PKCEサポートの認可フロー
authURL, verifier, err := client.GetAuthorizationURLWithPKCE("random-state-123")
if err != nil {
log.Fatal(err)
}
fmt.Printf("認可URL: %s\n", authURL)
fmt.Printf("Code Verifier (保存してください): %s\n", verifier)
// コールバック後、認可コードとverifierでトークン交換
// tokenResp, err := client.ExchangeCodeForToken("authorization-code", verifier)
}