- 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.
118 lines
3.4 KiB
Go
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
|
|
}
|