package oidc import ( "crypto/sha256" "encoding/base64" "errors" "log" "net/http" "net/url" "strings" "time" "gorm.io/gorm" "alterminal/internal/application" "alterminal/internal/auth" ) // tokenResponse 為 token 端點的成功回應(RFC 6749 §5.1;ID token 與 // refresh token 僅在對應條件成立時出現——ID token 於簽發對象為使用者 // 且 scope 含 openid 時、refresh token 於 scope 含 offline_access 時, // OIDC Core §3.1.3.3)。 type tokenResponse struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int64 `json:"expires_in"` Scope string `json:"scope"` IDToken string `json:"id_token,omitempty"` RefreshToken string `json:"refresh_token,omitempty"` } // tokenError 為 RFC 6749 §5.2 的錯誤回應格式。 type tokenError struct { Error string `json:"error"` ErrorDescription string `json:"error_description,omitempty"` } // writeTokenError 輸出 token 端點錯誤;client 認證失敗(invalid_client) // 回 401,其餘依規格回 400。 func writeTokenError(w http.ResponseWriter, status int, code, description string) { w.Header().Set("Cache-Control", "no-store") w.Header().Set("Pragma", "no-cache") if status == http.StatusUnauthorized { // 以 Basic 認證的請求須提示 Basic(RFC 6749 §5.2),一律附上不影響。 w.Header().Set("WWW-Authenticate", `Basic realm="alterminal"`) } auth.WriteJSON(w, status, tokenError{Error: code, ErrorDescription: description}) } // TokenHandler 處理 POST /token(RFC 6749 §3.2):以授權碼(§4.1.3) // 或 refresh token(§6)換發 Access/ID/Refresh Token。 func TokenHandler(db *gorm.DB, issuer string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.Header().Set("Allow", http.MethodPost) writeTokenError(w, http.StatusMethodNotAllowed, "invalid_request", "僅支援 POST") return } if ct := r.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/x-www-form-urlencoded") { writeTokenError(w, http.StatusBadRequest, "invalid_request", "Content-Type 須為 application/x-www-form-urlencoded") return } if err := r.ParseForm(); err != nil { writeTokenError(w, http.StatusBadRequest, "invalid_request", "無法解析表單內容") return } app := authenticateTokenClient(db, w, r) if app == nil { return } switch r.PostFormValue("grant_type") { case "authorization_code": tokenAuthorizationCode(db, issuer, w, r, app) case "refresh_token": tokenRefreshToken(db, issuer, w, r, app) case "": writeTokenError(w, http.StatusBadRequest, "invalid_request", "缺少 grant_type") default: writeTokenError(w, http.StatusBadRequest, "unsupported_grant_type", "不支援的 grant_type") } } } // authenticateTokenClient 驗證 Client 身分(RFC 6749 §2.3.1):client // 認證可經 HTTP Basic(推薦)或表單欄位,兩處同時出現的 client_id 必須 // 一致(§3.2.1)。機密式 Client 必須提出正確的 client secret;公開式 // Client 不持有 secret,僅以 client_id 識別(PKCE 承擔防護)。驗證失敗 // 已寫出 401 回應並回傳 nil。 func authenticateTokenClient(db *gorm.DB, w http.ResponseWriter, r *http.Request) *application.Application { basicID, basicSecret, hasBasic := r.BasicAuth() postID := r.PostFormValue("client_id") postSecret := r.PostFormValue("client_secret") // RFC 6749 §2.3.1 要求 Basic 中的 client_id/secret 先以表單編碼; // 本服務產生的識別值僅含 base64url 字元,解碼失敗時退回原值以相容 // 未編碼的實作。 if basicID != "" { if unescaped, err := url.QueryUnescape(basicID); err == nil { basicID = unescaped } if unescaped, err := url.QueryUnescape(basicSecret); err == nil { basicSecret = unescaped } } if hasBasic && postID != "" && basicID != postID { writeTokenError(w, http.StatusBadRequest, "invalid_request", "Basic 與表單的 client_id 不一致") return nil } clientID := postID if clientID == "" { clientID = basicID } if clientID == "" { writeTokenError(w, http.StatusUnauthorized, "invalid_client", "缺少 client_id") return nil } app, err := application.GetByClientID(db, clientID) if err != nil { // 查無 client 或查詢失敗一律 401,不洩漏 client 是否存在。 if !errors.Is(err, gorm.ErrRecordNotFound) { log.Printf("token: %v", err) } writeTokenError(w, http.StatusUnauthorized, "invalid_client", "client 認證失敗") return nil } secret := postSecret if hasBasic && secret == "" { secret = basicSecret } if !app.IsPublic() && !app.CheckSecret(secret) { writeTokenError(w, http.StatusUnauthorized, "invalid_client", "client 認證失敗") return nil } return app } // tokenAuthorizationCode 處理 grant_type=authorization_code(RFC 6749 // §4.1.3):兌換一次性授權碼,逐項比對兌換條件後簽發權杖。 func tokenAuthorizationCode(db *gorm.DB, issuer string, w http.ResponseWriter, r *http.Request, app *application.Application) { if !app.GrantTypes.Contains(application.GrantAuthorizationCode) { writeTokenError(w, http.StatusBadRequest, "unauthorized_client", "應用程式未啟用授權碼流程") return } code := r.PostFormValue("code") if code == "" { writeTokenError(w, http.StatusBadRequest, "invalid_request", "缺少 code") return } ac, err := GetAuthorizationCode(db, code) if err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { log.Printf("token: %v", err) } writeTokenError(w, http.StatusBadRequest, "invalid_grant", "授權碼無效") return } if ac.UsedAt != nil { // 授權碼重用:撤銷其發行的一切 refresh token(RFC 6749 §4.1.2 // 一次性要求;OAuth 2.0 Security BCP §4.5.3.3 的防護)。 if err := RevokeRefreshTokensByAuthorization(db, ac.ID); err != nil { log.Printf("token: %v", err) } writeTokenError(w, http.StatusBadRequest, "invalid_grant", "授權碼無效") return } if ac.ExpiresAt.Before(time.Now()) { writeTokenError(w, http.StatusBadRequest, "invalid_grant", "授權碼已過期") return } // 授權碼與 client 及 redirect_uri 的綁定逐項比對(RFC 6749 §4.1.3); // 不符一律回 invalid_grant,不洩漏原因。 if ac.ApplicationID != app.ID || r.PostFormValue("redirect_uri") != ac.RedirectURI { writeTokenError(w, http.StatusBadRequest, "invalid_grant", "授權碼無效") return } // PKCE(RFC 7636 §4.6):發碼時有 challenge 者,兌換必須提出比對 // 相符的 code_verifier。 if ac.CodeChallenge != "" { verifier := r.PostFormValue("code_verifier") if !validCodeVerifier(verifier) { writeTokenError(w, http.StatusBadRequest, "invalid_request", "code_verifier 格式無效") return } if pkceChallenge(verifier) != ac.CodeChallenge { writeTokenError(w, http.StatusBadRequest, "invalid_grant", "PKCE 驗證失敗") return } } ok, err := ConsumeAuthorizationCode(db, ac.ID) if err != nil { log.Printf("token: %v", err) writeTokenError(w, http.StatusInternalServerError, "", "") return } if !ok { // 並發兌換的輸家;勝者已完成撤銷防護,比照重用處理。 writeTokenError(w, http.StatusBadRequest, "invalid_grant", "授權碼無效") return } var u auth.User if err := db.First(&u, ac.UserID).Error; err != nil { log.Printf("token: 查詢使用者 %d: %v", ac.UserID, err) writeTokenError(w, http.StatusInternalServerError, "", "") return } issueTokenResponse(db, issuer, w, &u, app, ac.Scope, ac.Nonce, ac.ID, ac.AuthTime) } // tokenRefreshToken 處理 grant_type=refresh_token(RFC 6749 §6):以 // refresh token 換發新權杖組,舊 token 立即輪替作廢;偵測到重用已輪替 // 的 token 時撤銷該使用者於該應用程式的全部 refresh token。 func tokenRefreshToken(db *gorm.DB, issuer string, w http.ResponseWriter, r *http.Request, app *application.Application) { if !app.GrantTypes.Contains(application.GrantRefreshToken) { writeTokenError(w, http.StatusBadRequest, "unauthorized_client", "應用程式未啟用 refresh_token") return } token := r.PostFormValue("refresh_token") if token == "" { writeTokenError(w, http.StatusBadRequest, "invalid_request", "缺少 refresh_token") return } rt, err := GetRefreshToken(db, token) if err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { log.Printf("token: %v", err) } writeTokenError(w, http.StatusBadRequest, "invalid_grant", "refresh token 無效") return } if rt.RotatedAt != nil { // 已輪替的 token 再次出現即為重用:整鏈撤銷(OAuth 2.0 // Security BCP §4.14.2)。 if err := RevokeRefreshTokensFor(db, rt.UserID, rt.ApplicationID); err != nil { log.Printf("token: %v", err) } writeTokenError(w, http.StatusBadRequest, "invalid_grant", "refresh token 重用,相關權杖已撤銷") return } if rt.RevokedAt != nil || rt.ExpiresAt.Before(time.Now()) { writeTokenError(w, http.StatusBadRequest, "invalid_grant", "refresh token 已失效") return } if rt.ApplicationID != app.ID { writeTokenError(w, http.StatusBadRequest, "invalid_grant", "refresh token 無效") return } // RFC 6749 §6:請求可縮小 scope,不可擴大。 scope := rt.Scope if req := strings.TrimSpace(r.PostFormValue("scope")); req != "" { if !scopeCovered(rt.Scope, req) { writeTokenError(w, http.StatusBadRequest, "invalid_scope", "請求的 scope 超出原授權範圍") return } scope = strings.Join(normalizeScope(req), " ") } ok, err := RotateRefreshToken(db, rt.ID) if err != nil { log.Printf("token: %v", err) writeTokenError(w, http.StatusInternalServerError, "", "") return } if !ok { writeTokenError(w, http.StatusBadRequest, "invalid_grant", "refresh token 無效") return } var u auth.User if err := db.First(&u, rt.UserID).Error; err != nil { log.Printf("token: 查詢使用者 %d: %v", rt.UserID, err) writeTokenError(w, http.StatusInternalServerError, "", "") return } issueTokenResponse(db, issuer, w, &u, app, scope, "", rt.AuthorizationID, rt.AuthTime) } // issueTokenResponse 簽發權杖組並寫出成功回應:Access Token 必發;scope // 含 openid 時簽發 ID token;scope 含 offline_access 時簽發 refresh // token 並作廢舊授權碼鏈的後繼(由輪替模型保證單一現行 token)。 // authorizationID 為本次授權鏈的源頭授權碼 ID,refresh token 沿用記錄。 func issueTokenResponse(db *gorm.DB, issuer string, w http.ResponseWriter, u *auth.User, app *application.Application, scope, nonce string, authorizationID uint, authTime time.Time) { access, err := IssueAccessToken(db, issuer, u.ID, app, scope) if err != nil { log.Printf("token: %v", err) writeTokenError(w, http.StatusInternalServerError, "", "") return } resp := tokenResponse{ AccessToken: access, TokenType: "Bearer", ExpiresIn: int64(accessTokenTTL.Seconds()), Scope: scope, } if scopeHas(scope, "openid") { idToken, err := IssueIDToken(db, issuer, u, app, scope, nonce, authTime) if err != nil { log.Printf("token: %v", err) writeTokenError(w, http.StatusInternalServerError, "", "") return } resp.IDToken = idToken } if scopeHas(scope, "offline_access") { _, plain, err := NewRefreshToken(db, app.ID, u.ID, authorizationID, scope, authTime) if err != nil { log.Printf("token: %v", err) writeTokenError(w, http.StatusInternalServerError, "", "") return } resp.RefreshToken = plain } w.Header().Set("Cache-Control", "no-store") w.Header().Set("Pragma", "no-cache") auth.WriteJSON(w, http.StatusOK, resp) } // validCodeVerifier 檢查 code_verifier 格式(RFC 7636 §4.1): // 43–128 個字元,僅含 [A-Za-z0-9-._~]。 func validCodeVerifier(v string) bool { if len(v) < 43 || len(v) > 128 { return false } for _, c := range v { switch { case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9': case c == '-' || c == '.' || c == '_' || c == '~': default: return false } } return true } // pkceChallenge 計算 code_verifier 的 S256 challenge(RFC 7636 §4.2): // BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))。 func pkceChallenge(verifier string) string { sum := sha256.Sum256([]byte(verifier)) return base64.RawURLEncoding.EncodeToString(sum[:]) }