- Improve health check logic in CI workflows for both HTTP and gRPC readiness of the Tai service, ensuring clearer error reporting if the service fails to start. - Update the OAuth Device Flow implementation to support additional claims during device authorization, enhancing the flexibility of the authorization process. - Refactor the `AuthorizeDevice` method to accept extra claims, allowing for more detailed user context during authorization. - Introduce a new utility function to extract bearer tokens from requests, streamlining token handling across the OpenAPI service. These changes enhance the robustness of service readiness checks and improve the OAuth device authorization flow, contributing to a more reliable and flexible authentication mechanism.
122 lines
3.5 KiB
Go
122 lines
3.5 KiB
Go
package oauth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
|
)
|
|
|
|
const userCodeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
|
|
|
|
// DeviceAuthorization initiates the device authorization flow (RFC 8628).
|
|
func (s *Service) DeviceAuthorization(ctx context.Context, clientID string, scope string) (*types.DeviceAuthorizationResponse, error) {
|
|
if !s.config.Features.DeviceFlowEnabled {
|
|
return nil, &types.ErrorResponse{
|
|
Code: types.ErrorUnsupportedGrantType,
|
|
ErrorDescription: "Device flow is not enabled",
|
|
}
|
|
}
|
|
|
|
client, err := s.clientProvider.GetClientByID(ctx, clientID)
|
|
if err != nil || client == nil {
|
|
return nil, &types.ErrorResponse{
|
|
Code: types.ErrorInvalidClient,
|
|
ErrorDescription: "Invalid client",
|
|
}
|
|
}
|
|
|
|
if !clientSupportsGrantType(client, types.GrantTypeDeviceCode) {
|
|
return nil, &types.ErrorResponse{
|
|
Code: types.ErrorUnauthorizedClient,
|
|
ErrorDescription: "Client does not support device code grant",
|
|
}
|
|
}
|
|
|
|
deviceCode, err := s.generateToken("dc", clientID)
|
|
if err != nil {
|
|
return nil, &types.ErrorResponse{
|
|
Code: types.ErrorServerError,
|
|
ErrorDescription: "Failed to generate device code",
|
|
}
|
|
}
|
|
|
|
userCode, err := s.generateUserCode()
|
|
if err != nil {
|
|
return nil, &types.ErrorResponse{
|
|
Code: types.ErrorServerError,
|
|
ErrorDescription: "Failed to generate user code",
|
|
}
|
|
}
|
|
|
|
if err := s.storeDeviceCode(deviceCode, userCode, clientID, scope); err != nil {
|
|
return nil, &types.ErrorResponse{
|
|
Code: types.ErrorServerError,
|
|
ErrorDescription: "Failed to store device code",
|
|
}
|
|
}
|
|
|
|
verificationURI := fmt.Sprintf("%s/auth/device", s.config.IssuerURL)
|
|
verificationURIComplete := fmt.Sprintf("%s?user_code=%s", verificationURI, userCode)
|
|
|
|
return &types.DeviceAuthorizationResponse{
|
|
DeviceCode: deviceCode,
|
|
UserCode: userCode,
|
|
VerificationURI: verificationURI,
|
|
VerificationURIComplete: verificationURIComplete,
|
|
ExpiresIn: int(s.config.Token.DeviceCodeLifetime.Seconds()),
|
|
Interval: int(s.config.Token.DeviceCodeInterval.Seconds()),
|
|
}, nil
|
|
}
|
|
|
|
// AuthorizeDevice allows an authenticated user to authorize a device code via user_code.
|
|
func (s *Service) AuthorizeDevice(ctx context.Context, userCode string, subject string, extraClaims ...map[string]interface{}) error {
|
|
if !s.config.Features.DeviceFlowEnabled {
|
|
return &types.ErrorResponse{
|
|
Code: types.ErrorUnsupportedGrantType,
|
|
ErrorDescription: "Device flow is not enabled",
|
|
}
|
|
}
|
|
|
|
normalized := strings.ToUpper(strings.ReplaceAll(userCode, "-", ""))
|
|
formatted := normalized
|
|
if len(normalized) == 8 {
|
|
formatted = normalized[:4] + "-" + normalized[4:]
|
|
}
|
|
|
|
var claims map[string]interface{}
|
|
if len(extraClaims) > 0 {
|
|
claims = extraClaims[0]
|
|
}
|
|
return s.authorizeDeviceCode(formatted, subject, claims)
|
|
}
|
|
|
|
// generateUserCode generates a user-friendly code formatted as XXXX-XXXX.
|
|
func (s *Service) generateUserCode() (string, error) {
|
|
length := s.config.Token.UserCodeLength
|
|
if length <= 0 {
|
|
length = 8
|
|
}
|
|
raw, err := gonanoid.Generate(userCodeAlphabet, length)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(raw) == 8 {
|
|
return raw[:4] + "-" + raw[4:], nil
|
|
}
|
|
return raw, nil
|
|
}
|
|
|
|
func clientSupportsGrantType(client *types.ClientInfo, grantType string) bool {
|
|
if client == nil || len(client.GrantTypes) == 0 {
|
|
return false
|
|
}
|
|
for _, gt := range client.GrantTypes {
|
|
if gt == grantType {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|