yao/openapi/oauth/device.go
Max 1c79908649 Enhance OAuth Device Flow implementation
- Add support for the OAuth Device Authorization Flow (RFC 8628) in the OpenAPI service, allowing devices with limited input capabilities to obtain authorization.
- Implement `DeviceAuthorization()` and `AuthorizeDevice()` methods to handle device and user code generation, storage, and authorization.
- Update the OAuth endpoints to include `/device/authorize` for user code authorization and fix the discovery endpoint path for device authorization.
- Introduce MongoDB service in CI workflows for testing and enhance the unit test workflow with Redis setup.
- Update Go module dependencies to include necessary packages for the new features.

This commit significantly advances the OAuth capabilities of the application, enabling a more flexible authorization process for devices.
2026-03-04 15:19:48 +08:00

118 lines
3.4 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) 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:]
}
return s.authorizeDeviceCode(formatted, subject)
}
// 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
}