mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-16 04:03:47 +03:00
244 lines
8.1 KiB
Go
244 lines
8.1 KiB
Go
package oidc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/ory/fosite"
|
|
"github.com/ory/fosite/handler/rfc8628"
|
|
"github.com/pocket-id/pocket-id/backend/internal/common"
|
|
"github.com/pocket-id/pocket-id/backend/internal/dto"
|
|
"github.com/pocket-id/pocket-id/backend/internal/model"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type deviceService struct {
|
|
provider fosite.OAuth2Provider
|
|
store *Store
|
|
userCodeStrategy rfc8628.UserCodeStrategy
|
|
authorizationService *authorizationService
|
|
claimsService *ClaimsService
|
|
auditLog AuditLogger
|
|
db *gorm.DB
|
|
}
|
|
|
|
func newDeviceService(
|
|
provider fosite.OAuth2Provider,
|
|
store *Store,
|
|
userCodeStrategy rfc8628.UserCodeStrategy,
|
|
authorizationService *authorizationService,
|
|
claimsService *ClaimsService,
|
|
auditLog AuditLogger,
|
|
db *gorm.DB,
|
|
) *deviceService {
|
|
return &deviceService{
|
|
provider: provider,
|
|
store: store,
|
|
userCodeStrategy: userCodeStrategy,
|
|
authorizationService: authorizationService,
|
|
claimsService: claimsService,
|
|
auditLog: auditLog,
|
|
db: db,
|
|
}
|
|
}
|
|
|
|
func (s *deviceService) createDeviceAuthorization(ctx context.Context, req *http.Request) (*dto.OidcDeviceAuthorizationResponseDto, fosite.Requester, error) {
|
|
request, err := s.provider.NewDeviceRequest(ctx, req)
|
|
if err != nil {
|
|
return nil, request, err
|
|
}
|
|
|
|
// Validate the requested scopes and resolve the resource indicator to an audience and the subset of requested scopes that may be granted
|
|
client := request.GetClient().(Client)
|
|
resource, err := request.GetResource()
|
|
if err != nil {
|
|
return nil, request, err
|
|
}
|
|
audience, grantedScopes, _, err := s.authorizationService.resolveGrant(ctx, client.GetID(), resource, request.GetRequestedScopes())
|
|
if err != nil {
|
|
if resource != "" && errors.Is(err, fosite.ErrAccessDenied) {
|
|
return nil, request, fosite.ErrInvalidTarget.WithHintf("The requested resource '%s' is invalid, missing, unknown, or malformed.", resource)
|
|
}
|
|
return nil, request, err
|
|
}
|
|
grantResourceIndicator(request, audience, grantedScopes)
|
|
|
|
session := NewEmptySession()
|
|
response, err := s.provider.NewDeviceResponse(ctx, request, session)
|
|
if err != nil {
|
|
return nil, request, err
|
|
}
|
|
|
|
return &dto.OidcDeviceAuthorizationResponseDto{
|
|
DeviceCode: response.GetDeviceCode(),
|
|
UserCode: response.GetUserCode(),
|
|
VerificationURI: response.GetVerificationURI(),
|
|
VerificationURIComplete: response.GetVerificationURIComplete(),
|
|
ExpiresIn: int(response.GetExpiresIn()),
|
|
Interval: response.GetInterval(),
|
|
}, request, nil
|
|
}
|
|
|
|
func (s *deviceService) acceptDeviceCode(ctx context.Context, userCode, userID, authenticationMethod string, authenticationTime time.Time, reauthenticationToken string, meta requestMeta) error {
|
|
request, userCodeSignature, err := s.deviceRequestFromUserCode(ctx, userCode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// A user code may be approved only once. Rejecting an already-approved code prevents a second
|
|
// logged-in user from rebinding a pending device authorization to themselves before the device
|
|
// polls for its token.
|
|
if request.GetUserCodeState() != fosite.UserCodeUnused {
|
|
return &common.OidcInvalidDeviceCodeError{}
|
|
}
|
|
|
|
client := request.GetClient().(Client)
|
|
var user model.User
|
|
if err = s.db.WithContext(ctx).Preload("UserGroups").First(&user, "id = ?", userID).Error; err != nil {
|
|
return err
|
|
}
|
|
if !IsUserGroupAllowedToAuthorize(user, client.OidcClient) {
|
|
return fosite.ErrAccessDenied.WithHint("You are not allowed to access this service.")
|
|
}
|
|
|
|
resource, err := request.GetResource()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
audience, grantedScopes, consentKeys, err := s.authorizationService.resolveGrant(ctx, client.GetID(), resource, request.GetRequestedScopes())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
grantResourceIndicator(request, audience, grantedScopes)
|
|
|
|
return withTx(ctx, s.db, func(ctx context.Context) error {
|
|
if client.RequiresReauthentication {
|
|
if reauthenticationToken == "" || s.authorizationService == nil || s.authorizationService.reauth == nil {
|
|
return &common.ReauthenticationRequiredError{}
|
|
}
|
|
|
|
reauthenticatedAt, err := s.authorizationService.reauth.ConsumeReauthenticationToken(ctx, dbFromContext(ctx, s.db), reauthenticationToken, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
authenticationTime = reauthenticatedAt
|
|
}
|
|
if authenticationTime.IsZero() {
|
|
authenticationTime = time.Now().UTC()
|
|
}
|
|
|
|
session := NewAuthenticatedSession(userID, authenticationMethod, authenticationTime, request.GetRequestedAt())
|
|
|
|
if err = s.claimsService.applyIDTokenClaims(ctx, session, request.GetGrantedScopes()); err != nil {
|
|
return err
|
|
}
|
|
request.SetSession(session)
|
|
|
|
hasAlreadyAuthorizedClient, err := s.authorizationService.consent(ctx, userID, client.GetID(), consentKeys)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
event := model.AuditLogEventDeviceCodeAuthorization
|
|
if !hasAlreadyAuthorizedClient {
|
|
event = model.AuditLogEventNewDeviceCodeAuthorization
|
|
}
|
|
s.auditLog.Create(ctx, event, meta.IPAddress, meta.UserAgent, userID, model.AuditLogData{"clientName": client.Name}, dbFromContext(ctx, s.db))
|
|
|
|
deviceCodeSignature, err := s.store.AcceptDeviceCodeSessionByUserCodeSignature(ctx, userCodeSignature, request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if request.GetGrantedScopes().Has("openid") {
|
|
if err := s.store.CreateOpenIDConnectSession(ctx, deviceCodeSignature, request); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func (s *deviceService) getDeviceCodeInfo(ctx context.Context, userCode, userID string) (*dto.DeviceCodeInfoDto, error) {
|
|
request, _, err := s.deviceRequestFromUserCode(ctx, userCode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client := request.GetClient().(Client)
|
|
resource, err := request.GetResource()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
authorizationRequired := true
|
|
if userID != "" {
|
|
_, _, consentKeys, err := s.authorizationService.resolveGrant(ctx, client.GetID(), resource, request.GetRequestedScopes())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hasAuthorizedClient, err := s.authorizationService.hasAuthorizedClient(ctx, client.GetID(), userID, consentKeys)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// The device flow has no per-request prompt parameter, so consent depends only on prior authorization and the client's skip-consent setting
|
|
authorizationRequired = consentRequired(hasAuthorizedClient, client.SkipConsent, nil)
|
|
}
|
|
|
|
scope := request.GetRequestedScopes()
|
|
if scope == nil {
|
|
scope = []string{}
|
|
}
|
|
|
|
// Resolve friendly names for the requested custom-API permissions so the device consent screen matches the browser flow
|
|
scopeInfo, err := s.authorizationService.resolveScopeInfoForRequest(ctx, resource, scope)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Always serialize a possibly empty array rather than null
|
|
if scopeInfo == nil {
|
|
scopeInfo = []dto.ScopeInfoDto{}
|
|
}
|
|
|
|
return &dto.DeviceCodeInfoDto{
|
|
Client: dto.OidcClientMetaDataDto{
|
|
ID: client.ID,
|
|
Name: client.Name,
|
|
HasLogo: client.HasLogo(),
|
|
HasDarkLogo: client.HasDarkLogo(),
|
|
LaunchURL: client.LaunchURL,
|
|
RequiresReauthentication: client.RequiresReauthentication,
|
|
},
|
|
Scope: scope,
|
|
ScopeInfo: scopeInfo,
|
|
AuthorizationRequired: authorizationRequired,
|
|
ReauthenticationRequired: client.RequiresReauthentication,
|
|
}, nil
|
|
}
|
|
|
|
func (s *deviceService) deviceRequestFromUserCode(ctx context.Context, userCode string) (fosite.DeviceRequester, string, error) {
|
|
userCodeSignature, err := s.userCodeStrategy.UserCodeSignature(ctx, userCode)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
request, err := s.store.GetDeviceCodeSessionByUserCodeSignature(ctx, userCodeSignature)
|
|
if errors.Is(err, fosite.ErrNotFound) {
|
|
return nil, "", &common.OidcInvalidDeviceCodeError{}
|
|
}
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
if err = s.userCodeStrategy.ValidateUserCode(ctx, request, userCode); err != nil {
|
|
if errors.Is(err, fosite.ErrDeviceExpiredToken) {
|
|
return nil, "", &common.OidcDeviceCodeExpiredError{}
|
|
}
|
|
return nil, "", err
|
|
}
|
|
|
|
return request, userCodeSignature, nil
|
|
}
|