authelia/internal/model/oidc.go
James Elliott 0a970aef8a
feat(oidc): persistent storage (#2965)
This moves the OpenID Connect storage from memory into the SQL storage, making it persistent and allowing it to be used with clustered deployments like the rest of Authelia.
2022-04-07 15:33:53 +10:00

229 lines
7.6 KiB
Go

package model
import (
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"net/url"
"time"
"github.com/google/uuid"
"github.com/ory/fosite"
"github.com/ory/fosite/handler/openid"
"github.com/authelia/authelia/v4/internal/utils"
)
// NewOAuth2ConsentSession creates a new OAuth2ConsentSession.
func NewOAuth2ConsentSession(subject uuid.UUID, r fosite.Requester) (consent *OAuth2ConsentSession, err error) {
consent = &OAuth2ConsentSession{
ClientID: r.GetClient().GetID(),
Subject: subject,
Form: r.GetRequestForm().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
}
// NewOAuth2SessionFromRequest creates a new OAuth2Session from a signature and fosite.Requester.
func NewOAuth2SessionFromRequest(signature string, r fosite.Requester) (session *OAuth2Session, err error) {
var (
subject string
openidSession *OpenIDSession
sessData []byte
)
openidSession = r.GetSession().(*OpenIDSession)
if openidSession == nil {
return nil, errors.New("unexpected session type")
}
subject = openidSession.GetSubject()
if sessData, err = json.Marshal(openidSession); err != nil {
return nil, err
}
return &OAuth2Session{
ChallengeID: openidSession.ChallengeID,
RequestID: r.GetID(),
ClientID: r.GetClient().GetID(),
Signature: signature,
RequestedAt: r.GetRequestedAt(),
Subject: subject,
RequestedScopes: StringSlicePipeDelimited(r.GetRequestedScopes()),
GrantedScopes: StringSlicePipeDelimited(r.GetGrantedScopes()),
RequestedAudience: StringSlicePipeDelimited(r.GetRequestedAudience()),
GrantedAudience: StringSlicePipeDelimited(r.GetGrantedAudience()),
Active: true,
Revoked: false,
Form: r.GetRequestForm().Encode(),
Session: sessData,
}, 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,
}
}
// 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.UUID `db:"subject"`
Authorized bool `db:"authorized"`
Granted bool `db:"granted"`
RequestedAt time.Time `db:"requested_at"`
RespondedAt *time.Time `db:"responded_at"`
ExpiresAt *time.Time `db:"expires_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"`
}
// 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)
}
// 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
}
// CanGrant returns true if the user has responded to the consent session, it was authorized, and it either hast not
// previously been granted or the ability to grant has not expired.
func (s OAuth2ConsentSession) CanGrant() bool {
if !s.Responded() {
return false
}
if s.Granted && (s.ExpiresAt == nil || s.ExpiresAt.Before(time.Now())) {
return false
}
return true
}
// 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
}
// Responded returns true if the user has responded to the consent session.
func (s OAuth2ConsentSession) Responded() bool {
return s.RespondedAt != nil
}
// 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"`
}
// OpenIDSession holds OIDC Session information.
type OpenIDSession struct {
*openid.DefaultSession `json:"id_token"`
ChallengeID uuid.UUID `db:"challenge_id"`
ClientID string
Extra map[string]interface{} `json:"extra"`
}
// OAuth2Session represents a OAuth2.0 session.
type OAuth2Session struct {
ID int `db:"id"`
ChallengeID uuid.UUID `db:"challenge_id"`
RequestID string `db:"request_id"`
ClientID string `db:"client_id"`
Signature string `db:"signature"`
RequestedAt time.Time `db:"requested_at"`
Subject string `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 = subject
}
// ToRequest converts an OAuth2Session into a fosite.Request given a fosite.Session and fosite.Storage.
func (s OAuth2Session) ToRequest(ctx context.Context, session fosite.Session, store fosite.Storage) (request *fosite.Request, err error) {
sessionData := s.Session
if session != nil {
if err = json.Unmarshal(sessionData, session); err != nil {
return nil, err
}
}
client, err := store.GetClient(ctx, s.ClientID)
if err != nil {
return nil, err
}
values, err := url.ParseQuery(s.Form)
if err != nil {
return nil, err
}
return &fosite.Request{
ID: s.RequestID,
RequestedAt: s.RequestedAt,
Client: client,
RequestedScope: fosite.Arguments(s.RequestedScopes),
GrantedScope: fosite.Arguments(s.GrantedScopes),
RequestedAudience: fosite.Arguments(s.RequestedAudience),
GrantedAudience: fosite.Arguments(s.GrantedAudience),
Form: values,
Session: session,
}, nil
}