authelia/internal/model/oidc.go

402 lines
15 KiB
Go

package model
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/json"
"fmt"
"net/url"
"strings"
"time"
oauthelia2 "authelia.com/provider/oauth2"
"github.com/google/uuid"
"github.com/authelia/authelia/v4/internal/utils"
)
// NewOAuth2ConsentSession creates a new OAuth2ConsentSession.
func NewOAuth2ConsentSession(subject uuid.UUID, r oauthelia2.Requester) (consent *OAuth2ConsentSession, err error) {
return NewOAuth2ConsentSessionWithForm(subject, r, r.GetRequestForm())
}
// NewOAuth2ConsentSessionWithForm creates a new OAuth2ConsentSession with a custom form parameter.
func NewOAuth2ConsentSessionWithForm(subject uuid.UUID, r oauthelia2.Requester, form url.Values) (consent *OAuth2ConsentSession, err error) {
consent = &OAuth2ConsentSession{
ClientID: r.GetClient().GetID(),
Subject: NullUUID(subject),
Form: form.Encode(),
RequestedAt: r.GetRequestedAt(),
RequestedScopes: StringSlicePipeDelimited(r.GetRequestedScopes()),
RequestedAudience: StringSlicePipeDelimited(r.GetRequestedAudience()),
GrantedScopes: StringSlicePipeDelimited(r.GetGrantedScopes()),
GrantedAudience: StringSlicePipeDelimited(r.GetGrantedAudience()),
}
if consent.ChallengeID, err = uuid.NewRandom(); err != nil {
return nil, err
}
return consent, nil
}
// NewOAuth2BlacklistedJTI creates a new OAuth2BlacklistedJTI.
func NewOAuth2BlacklistedJTI(jti string, exp time.Time) (jtiBlacklist OAuth2BlacklistedJTI) {
return OAuth2BlacklistedJTI{
Signature: fmt.Sprintf("%x", sha256.Sum256([]byte(jti))),
ExpiresAt: exp,
}
}
// NewOAuth2SessionFromRequest creates a new OAuth2Session from a signature and oauthelia2.Requester.
func NewOAuth2SessionFromRequest(signature string, r oauthelia2.Requester) (session *OAuth2Session, err error) {
if r == nil {
return nil, fmt.Errorf("failed to create new *model.OAuth2Session: the oauthelia2.Requester was nil")
}
var (
subject sql.NullString
s OpenIDSession
ok bool
sessionData []byte
)
s, ok = r.GetSession().(OpenIDSession)
if !ok {
return nil, fmt.Errorf("failed to create new *model.OAuth2Session: the session type OpenIDSession was expected but the type '%T' was used", r.GetSession())
}
subject = sql.NullString{String: s.GetSubject()}
subject.Valid = len(subject.String) > 0
if sessionData, err = json.Marshal(s); err != nil {
return nil, fmt.Errorf("failed to create new *model.OAuth2Session: an error was returned while attempting to marshal the session data to json: %w", err)
}
requested, granted := r.GetRequestedScopes(), r.GetGrantedScopes()
if requested == nil {
requested = oauthelia2.Arguments{}
}
if granted == nil {
granted = oauthelia2.Arguments{}
}
return &OAuth2Session{
ChallengeID: s.GetChallengeID(),
RequestID: r.GetID(),
ClientID: r.GetClient().GetID(),
Signature: signature,
RequestedAt: r.GetRequestedAt(),
Subject: subject,
RequestedScopes: StringSlicePipeDelimited(requested),
GrantedScopes: StringSlicePipeDelimited(granted),
RequestedAudience: StringSlicePipeDelimited(r.GetRequestedAudience()),
GrantedAudience: StringSlicePipeDelimited(r.GetGrantedAudience()),
Active: true,
Revoked: false,
Form: r.GetRequestForm().Encode(),
Session: sessionData,
}, nil
}
// NewOAuth2PARContext creates a new Pushed Authorization Request Context as a OAuth2PARContext.
func NewOAuth2PARContext(contextID string, r oauthelia2.AuthorizeRequester) (context *OAuth2PARContext, err error) {
var (
s OpenIDSession
ok bool
req *oauthelia2.AuthorizeRequest
session []byte
)
if s, ok = r.GetSession().(OpenIDSession); !ok {
return nil, fmt.Errorf("failed to create new PAR context: can't assert type '%T' to an *OAuth2Session", r.GetSession())
}
if session, err = json.Marshal(s); err != nil {
return nil, err
}
var handled StringSlicePipeDelimited
if req, ok = r.(*oauthelia2.AuthorizeRequest); ok {
handled = StringSlicePipeDelimited(req.HandledResponseTypes)
}
return &OAuth2PARContext{
Signature: contextID,
RequestID: r.GetID(),
ClientID: r.GetClient().GetID(),
RequestedAt: r.GetRequestedAt(),
Scopes: StringSlicePipeDelimited(r.GetRequestedScopes()),
Audience: StringSlicePipeDelimited(r.GetRequestedAudience()),
HandledResponseTypes: handled,
ResponseMode: string(r.GetResponseMode()),
DefaultResponseMode: string(r.GetDefaultResponseMode()),
Revoked: false,
Form: r.GetRequestForm().Encode(),
Session: session,
}, nil
}
// OAuth2ConsentPreConfig stores information about an OAuth2.0 Pre-Configured Consent.
type OAuth2ConsentPreConfig struct {
ID int64 `db:"id"`
ClientID string `db:"client_id"`
Subject uuid.UUID `db:"subject"`
CreatedAt time.Time `db:"created_at"`
ExpiresAt sql.NullTime `db:"expires_at"`
Revoked bool `db:"revoked"`
Scopes StringSlicePipeDelimited `db:"scopes"`
Audience StringSlicePipeDelimited `db:"audience"`
}
// HasExactGrants returns true if the granted audience and scopes of this consent pre-configuration matches exactly with
// another audience and set of scopes.
func (s *OAuth2ConsentPreConfig) HasExactGrants(scopes, audience []string) (has bool) {
return s.HasExactGrantedScopes(scopes) && s.HasExactGrantedAudience(audience)
}
// HasExactGrantedAudience returns true if the granted audience of this consent matches exactly with another audience.
func (s *OAuth2ConsentPreConfig) HasExactGrantedAudience(audience []string) (has bool) {
return !utils.IsStringSlicesDifferent(s.Audience, audience)
}
// HasExactGrantedScopes returns true if the granted scopes of this consent matches exactly with another set of scopes.
func (s *OAuth2ConsentPreConfig) HasExactGrantedScopes(scopes []string) (has bool) {
return !utils.IsStringSlicesDifferent(s.Scopes, scopes)
}
// CanConsent returns true if this pre-configuration can still provide consent.
func (s *OAuth2ConsentPreConfig) CanConsent() bool {
return !s.Revoked && (!s.ExpiresAt.Valid || s.ExpiresAt.Time.After(time.Now()))
}
// OAuth2ConsentSession stores information about an OAuth2.0 Consent.
type OAuth2ConsentSession struct {
ID int `db:"id"`
ChallengeID uuid.UUID `db:"challenge_id"`
ClientID string `db:"client_id"`
Subject uuid.NullUUID `db:"subject"`
Authorized bool `db:"authorized"`
Granted bool `db:"granted"`
RequestedAt time.Time `db:"requested_at"`
RespondedAt sql.NullTime `db:"responded_at"`
Form string `db:"form_data"`
RequestedScopes StringSlicePipeDelimited `db:"requested_scopes"`
GrantedScopes StringSlicePipeDelimited `db:"granted_scopes"`
RequestedAudience StringSlicePipeDelimited `db:"requested_audience"`
GrantedAudience StringSlicePipeDelimited `db:"granted_audience"`
PreConfiguration sql.NullInt64
}
// Grant grants the requested scopes and audience.
func (s *OAuth2ConsentSession) Grant() {
s.GrantedScopes = s.RequestedScopes
s.GrantedAudience = s.RequestedAudience
}
// HasExactGrants returns true if the granted audience and scopes of this consent matches exactly with another
// audience and set of scopes.
func (s *OAuth2ConsentSession) HasExactGrants(scopes, audience []string) (has bool) {
return s.HasExactGrantedScopes(scopes) && s.HasExactGrantedAudience(audience)
}
// HasExactGrantedAudience returns true if the granted audience of this consent matches exactly with another audience.
func (s *OAuth2ConsentSession) HasExactGrantedAudience(audience []string) (has bool) {
return !utils.IsStringSlicesDifferent(s.GrantedAudience, audience)
}
// HasExactGrantedScopes returns true if the granted scopes of this consent matches exactly with another set of scopes.
func (s *OAuth2ConsentSession) HasExactGrantedScopes(scopes []string) (has bool) {
return !utils.IsStringSlicesDifferent(s.GrantedScopes, scopes)
}
// Responded returns true if the user has responded to the consent session.
func (s *OAuth2ConsentSession) Responded() bool {
return s.RespondedAt.Valid
}
// IsAuthorized returns true if the user has responded to the consent session and it was authorized.
func (s *OAuth2ConsentSession) IsAuthorized() bool {
return s.Responded() && s.Authorized
}
// IsDenied returns true if the user has responded to the consent session and it was not authorized.
func (s *OAuth2ConsentSession) IsDenied() bool {
return s.Responded() && !s.Authorized
}
// CanGrant returns true if the session can still grant a token. This is NOT indicative of if there is a user response
// to this consent request or if the user rejected the consent request.
func (s *OAuth2ConsentSession) CanGrant() bool {
if !s.Subject.Valid || s.Granted {
return false
}
return true
}
// GetForm returns the form.
func (s *OAuth2ConsentSession) GetForm() (form url.Values, err error) {
return url.ParseQuery(s.Form)
}
// OAuth2BlacklistedJTI represents a blacklisted JTI used with OAuth2.0.
type OAuth2BlacklistedJTI struct {
ID int `db:"id"`
Signature string `db:"signature"`
ExpiresAt time.Time `db:"expires_at"`
}
// OAuth2Session represents a OAuth2.0 session.
type OAuth2Session struct {
ID int `db:"id"`
ChallengeID uuid.NullUUID `db:"challenge_id"`
RequestID string `db:"request_id"`
ClientID string `db:"client_id"`
Signature string `db:"signature"`
RequestedAt time.Time `db:"requested_at"`
Subject sql.NullString `db:"subject"`
RequestedScopes StringSlicePipeDelimited `db:"requested_scopes"`
GrantedScopes StringSlicePipeDelimited `db:"granted_scopes"`
RequestedAudience StringSlicePipeDelimited `db:"requested_audience"`
GrantedAudience StringSlicePipeDelimited `db:"granted_audience"`
Active bool `db:"active"`
Revoked bool `db:"revoked"`
Form string `db:"form_data"`
Session []byte `db:"session_data"`
}
// SetSubject implements an interface required for RFC7523.
func (s *OAuth2Session) SetSubject(subject string) {
s.Subject = sql.NullString{String: subject, Valid: len(subject) > 0}
}
// ToRequest converts an OAuth2Session into a oauthelia2.Request given a oauthelia2.Session and oauthelia2.Storage.
func (s *OAuth2Session) ToRequest(ctx context.Context, session oauthelia2.Session, store oauthelia2.Storage) (request *oauthelia2.Request, err error) {
sessionData := s.Session
if session != nil {
if err = json.Unmarshal(sessionData, session); err != nil {
return nil, fmt.Errorf("error occurred while mapping OAuth 2.0 Session back to a Request while trying to unmarshal the JSON session data: %w", err)
}
}
client, err := store.GetClient(ctx, s.ClientID)
if err != nil {
return nil, fmt.Errorf("error occurred while mapping OAuth 2.0 Session back to a Request while trying to lookup the registered client: %w", err)
}
values, err := url.ParseQuery(s.Form)
if err != nil {
return nil, fmt.Errorf("error occurred while mapping OAuth 2.0 Session back to a Request while trying to parse the original form: %w", err)
}
return &oauthelia2.Request{
ID: s.RequestID,
RequestedAt: s.RequestedAt,
Client: client,
RequestedScope: oauthelia2.Arguments(s.RequestedScopes),
GrantedScope: oauthelia2.Arguments(s.GrantedScopes),
RequestedAudience: oauthelia2.Arguments(s.RequestedAudience),
GrantedAudience: oauthelia2.Arguments(s.GrantedAudience),
Form: values,
Session: session,
}, nil
}
// OAuth2PARContext holds relevant information about a Pushed Authorization Request in order to process the authorization.
type OAuth2PARContext struct {
ID int `db:"id"`
Signature string `db:"signature"`
RequestID string `db:"request_id"`
ClientID string `db:"client_id"`
RequestedAt time.Time `db:"requested_at"`
Scopes StringSlicePipeDelimited `db:"scopes"`
Audience StringSlicePipeDelimited `db:"audience"`
HandledResponseTypes StringSlicePipeDelimited `db:"handled_response_types"`
ResponseMode string `db:"response_mode"`
DefaultResponseMode string `db:"response_mode_default"`
Revoked bool `db:"revoked"`
Form string `db:"form_data"`
Session []byte `db:"session_data"`
}
func (par *OAuth2PARContext) ToAuthorizeRequest(ctx context.Context, session oauthelia2.Session, store oauthelia2.Storage) (request *oauthelia2.AuthorizeRequest, err error) {
if session != nil {
if err = json.Unmarshal(par.Session, session); err != nil {
return nil, fmt.Errorf("error occurred while mapping PAR context back to an Authorize Request while trying to unmarshal the JSON session data: %w", err)
}
}
var (
client oauthelia2.Client
form url.Values
)
if client, err = store.GetClient(ctx, par.ClientID); err != nil {
return nil, fmt.Errorf("error occurred while mapping PAR context back to an Authorize Request while trying to lookup the registered client: %w", err)
}
if form, err = url.ParseQuery(par.Form); err != nil {
return nil, fmt.Errorf("error occurred while mapping PAR context back to an Authorize Request while trying to parse the original form: %w", err)
}
request = oauthelia2.NewAuthorizeRequest()
request.Request = oauthelia2.Request{
ID: par.RequestID,
RequestedAt: par.RequestedAt,
Client: client,
RequestedScope: oauthelia2.Arguments(par.Scopes),
RequestedAudience: oauthelia2.Arguments(par.Audience),
Form: form,
Session: session,
}
request.State = form.Get("state")
if form.Has("redirect_uri") {
if request.RedirectURI, err = url.Parse(form.Get("redirect_uri")); err != nil {
return nil, fmt.Errorf("error occurred while mapping PAR context back to an Authorize Request while trying to parse the original redirect uri: %w", err)
}
}
if form.Has("response_type") {
request.ResponseTypes = oauthelia2.RemoveEmpty(strings.Split(form.Get("response_type"), " "))
}
if par.ResponseMode != "" {
request.ResponseMode = oauthelia2.ResponseModeType(par.ResponseMode)
}
if par.DefaultResponseMode != "" {
request.DefaultResponseMode = oauthelia2.ResponseModeType(par.DefaultResponseMode)
}
if len(par.HandledResponseTypes) != 0 {
request.HandledResponseTypes = oauthelia2.Arguments(par.HandledResponseTypes)
}
return request, nil
}
// OpenIDSession represents the types available for an oidc.Session that are required in the models package.
type OpenIDSession interface {
oauthelia2.Session
GetChallengeID() uuid.NullUUID
}