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.pull/3116/head
parent
06fd7105ea
commit
0a970aef8a
|
@ -232,7 +232,7 @@ Allows PKCE `plain` challenges when set to `true`.
|
|||
Some OpenID Connect Endpoints need to allow cross-origin resource sharing, however some are optional. This section allows
|
||||
you to configure the optional parts. We reply with CORS headers when the request includes the Origin header.
|
||||
|
||||
##### endpoints
|
||||
#### endpoints
|
||||
<div markdown="1">
|
||||
type: list(string)
|
||||
{: .label .label-config .label-purple }
|
||||
|
@ -523,7 +523,7 @@ individual user. Please use the claim `preferred_username` instead._
|
|||
This scope includes the groups the authentication backend reports the user is a member of in the token.
|
||||
|
||||
| Claim | JWT Type | Authelia Attribute | Description |
|
||||
|:------:|:-------------:|:------------------:|:----------------------:|
|
||||
|:------:|:-------------:|:------------------:|:------------------------------------------------------------------------------------------------------------------:|
|
||||
| groups | array[string] | groups | List of user's groups discovered via [authentication](https://www.authelia.com/docs/configuration/authentication/) |
|
||||
|
||||
### email
|
||||
|
@ -586,9 +586,9 @@ an example of the Authelia root URL which is also the OpenID Connect issuer.
|
|||
These endpoints can be utilized to discover other endpoints and metadata about the Authelia OP.
|
||||
|
||||
| Endpoint | Path |
|
||||
|:-------------:|:---------------------------------------------------------------:|
|
||||
| Discovery | https://auth.example.com/.well-known/openid-configuration |
|
||||
| Metadata | https://auth.example.com/.well-known/oauth-authorization-server |
|
||||
|:-----------------------------------------:|:---------------------------------------------------------------:|
|
||||
| [OpenID Connect Discovery] | https://auth.example.com/.well-known/openid-configuration |
|
||||
| [OAuth 2.0 Authorization Server Metadata] | https://auth.example.com/.well-known/oauth-authorization-server |
|
||||
|
||||
|
||||
### Discoverable Endpoints
|
||||
|
@ -596,19 +596,22 @@ These endpoints can be utilized to discover other endpoints and metadata about t
|
|||
These endpoints implement OpenID Connect elements.
|
||||
|
||||
| Endpoint | Path | Discovery Attribute |
|
||||
|:---------------:|:-----------------------------------------------:|:----------------------:|
|
||||
| JWKS | https://auth.example.com/jwks.json | jwks_uri |
|
||||
|:-------------------:|:-----------------------------------------------:|:----------------------:|
|
||||
| [JSON Web Key Sets] | https://auth.example.com/jwks.json | jwks_uri |
|
||||
| [Authorization] | https://auth.example.com/api/oidc/authorization | authorization_endpoint |
|
||||
| [Token] | https://auth.example.com/api/oidc/token | token_endpoint |
|
||||
| [Userinfo] | https://auth.example.com/api/oidc/userinfo | userinfo_endpoint |
|
||||
| [Introspection] | https://auth.example.com/api/oidc/introspection | introspection_endpoint |
|
||||
| [Revocation] | https://auth.example.com/api/oidc/revocation | revocation_endpoint |
|
||||
|
||||
[JSON Web Key Sets]: https://datatracker.ietf.org/doc/html/rfc7517#section-5
|
||||
[OpenID Connect]: https://openid.net/connect/
|
||||
[token lifespan]: https://docs.apigee.com/api-platform/antipatterns/oauth-long-expiration
|
||||
[OpenID Connect Discovery]: https://openid.net/specs/openid-connect-discovery-1_0.html
|
||||
[OAuth 2.0 Authorization Server Metadata]: https://datatracker.ietf.org/doc/html/rfc8414
|
||||
[Authorization]: https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
|
||||
[Token]: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
|
||||
[Userinfo]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
|
||||
[Introspection]: https://datatracker.ietf.org/doc/html/rfc7662
|
||||
[Revocation]: https://datatracker.ietf.org/doc/html/rfc7009
|
||||
[RFC8176]: https://datatracker.ietf.org/doc/html/rfc8176
|
||||
[token lifespan]: https://docs.apigee.com/api-platform/antipatterns/oauth-long-expiration
|
||||
|
|
|
@ -24,3 +24,4 @@ this instance if you wanted to downgrade to pre1 you would need to use an Authel
|
|||
| 1 | 4.33.0 | Initial migration managed version |
|
||||
| 2 | 4.34.0 | Webauthn - added webauthn_devices table, altered totp_config to include device created/used dates |
|
||||
| 3 | 4.34.2 | Webauthn - fix V2 migration kid column length and provide migration path for anyone on V2 |
|
||||
| 4 | 4.35.0 | Added OpenID Connect storage tables and opaque user identifier tables |
|
||||
|
|
|
@ -74,7 +74,7 @@ for which stage will have each feature, and may evolve over time:
|
|||
<td>Proof Key for Code Exchange (PKCE) for Authorization Code Flow</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="3" class="tbl-header tbl-beta-stage">beta4 <sup>1</sup></td>
|
||||
<td rowspan="8" class="tbl-header tbl-beta-stage">beta5 (4.35.0)</td>
|
||||
<td>Token Storage</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -83,6 +83,21 @@ for which stage will have each feature, and may evolve over time:
|
|||
<tr>
|
||||
<td class="tbl-beta-stage">Subject Storage</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tbl-beta-stage"><a href="https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes" target="_blank">Pairwise Subject Identifier Type</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tbl-beta-stage">Per-Client Consent Pre-Configuration</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tbl-beta-stage">Cross-Origin Resource Sharing Configuration</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tbl-beta-stage">Authentication Methods References Claim</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tbl-beta-stage">UUID v4 <code>sub</code> claim</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2" class="tbl-header tbl-beta-stage">beta5 <sup>1</sup></td>
|
||||
<td class="tbl-beta-stage">Prompt Handling</td>
|
||||
|
@ -91,7 +106,7 @@ for which stage will have each feature, and may evolve over time:
|
|||
<td class="tbl-beta-stage">Display Handling</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="5" class="tbl-header tbl-beta-stage">beta6 <sup>1</sup></td>
|
||||
<td rowspan="4" class="tbl-header tbl-beta-stage">beta6 <sup>1</sup></td>
|
||||
<td><a href="https://openid.net/specs/openid-connect-backchannel-1_0.html" target="_blank" rel="noopener noreferrer">Back-Channel Logout</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -103,9 +118,6 @@ for which stage will have each feature, and may evolve over time:
|
|||
<tr>
|
||||
<td class="tbl-beta-stage">Client Secrets Hashed in Configuration</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tbl-beta-stage">UUID or Random String for <code>sub</code> claim</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="tbl-header tbl-beta-stage">GA <sup>1</sup></td>
|
||||
<td class="tbl-beta-stage">General Availability after previous stages are vetted for bug fixes</td>
|
||||
|
|
|
@ -64,7 +64,7 @@ func getProviders() (providers middlewares.Providers, warnings []error, errors [
|
|||
sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
|
||||
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
|
||||
|
||||
oidcProvider, err := oidc.NewOpenIDConnectProvider(config.IdentityProviders.OIDC)
|
||||
oidcProvider, err := oidc.NewOpenIDConnectProvider(config.IdentityProviders.OIDC, storageProvider)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ type IdentityProvidersConfiguration struct {
|
|||
|
||||
// OpenIDConnectConfiguration configuration for OpenID Connect.
|
||||
type OpenIDConnectConfiguration struct {
|
||||
// This secret must be 32 bytes long.
|
||||
HMACSecret string `koanf:"hmac_secret"`
|
||||
IssuerPrivateKey string `koanf:"issuer_private_key"`
|
||||
|
||||
|
@ -49,9 +48,10 @@ type OpenIDConnectClientConfiguration struct {
|
|||
|
||||
Policy string `koanf:"authorization_policy"`
|
||||
|
||||
RedirectURIs []string `koanf:"redirect_uris"`
|
||||
|
||||
Audience []string `koanf:"audience"`
|
||||
Scopes []string `koanf:"scopes"`
|
||||
RedirectURIs []string `koanf:"redirect_uris"`
|
||||
GrantTypes []string `koanf:"grant_types"`
|
||||
ResponseTypes []string `koanf:"response_types"`
|
||||
ResponseModes []string `koanf:"response_modes"`
|
||||
|
|
|
@ -73,7 +73,7 @@ func FirstFactorPost(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
|
|||
|
||||
userSession := ctx.GetSession()
|
||||
newSession := session.NewDefaultUserSession()
|
||||
newSession.OIDCWorkflowSession = userSession.OIDCWorkflowSession
|
||||
newSession.ConsentChallengeID = userSession.ConsentChallengeID
|
||||
|
||||
// Reset all values from previous session except OIDC workflow before regenerating the cookie.
|
||||
if err = ctx.SaveSession(newSession); err != nil {
|
||||
|
@ -135,7 +135,7 @@ func FirstFactorPost(delayFunc middlewares.TimingAttackDelayFunc) middlewares.Re
|
|||
|
||||
successful = true
|
||||
|
||||
if userSession.OIDCWorkflowSession != nil {
|
||||
if userSession.ConsentChallengeID != nil {
|
||||
handleOIDCWorkflowResponse(ctx)
|
||||
} else {
|
||||
Handle1FAResponse(ctx, bodyJSON.TargetURL, bodyJSON.RequestMethod, userSession.Username, userSession.Groups)
|
||||
|
|
|
@ -2,18 +2,15 @@ package handlers
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ory/fosite"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authorization"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
"github.com/authelia/authelia/v4/internal/oidc"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
)
|
||||
|
||||
// OpenIDConnectAuthorizationGET handles GET requests to the OpenID Connect 1.0 Authorization endpoint.
|
||||
|
@ -23,7 +20,7 @@ func OpenIDConnectAuthorizationGET(ctx *middlewares.AutheliaCtx, rw http.Respons
|
|||
var (
|
||||
requester fosite.AuthorizeRequester
|
||||
responder fosite.AuthorizeResponder
|
||||
client *oidc.InternalClient
|
||||
client *oidc.Client
|
||||
authTime time.Time
|
||||
issuer string
|
||||
err error
|
||||
|
@ -43,7 +40,7 @@ func OpenIDConnectAuthorizationGET(ctx *middlewares.AutheliaCtx, rw http.Respons
|
|||
|
||||
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' is being processed", requester.GetID(), clientID)
|
||||
|
||||
if client, err = ctx.Providers.OpenIDConnect.Store.GetInternalClient(clientID); err != nil {
|
||||
if client, err = ctx.Providers.OpenIDConnect.Store.GetFullClient(clientID); err != nil {
|
||||
if errors.Is(err, fosite.ErrNotFound) {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: client was not found", requester.GetID(), clientID)
|
||||
} else {
|
||||
|
@ -65,31 +62,27 @@ func OpenIDConnectAuthorizationGET(ctx *middlewares.AutheliaCtx, rw http.Respons
|
|||
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
requestedScopes := requester.GetRequestedScopes()
|
||||
requestedAudience := requester.GetRequestedAudience()
|
||||
var subject uuid.UUID
|
||||
|
||||
isAuthInsufficient := !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel)
|
||||
if subject, err = ctx.Providers.OpenIDConnect.Store.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred retrieving subject for user '%s': %+v", requester.GetID(), client.GetID(), userSession.Username, err)
|
||||
|
||||
if isAuthInsufficient || (isConsentMissing(userSession.OIDCWorkflowSession, requestedScopes, requestedAudience)) {
|
||||
oidcAuthorizeHandleAuthorizationOrConsentInsufficient(ctx, userSession, client, isAuthInsufficient, rw, r, requester, issuer)
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not retrieve the subject."))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
extraClaims := oidcGrantRequests(requester, requestedScopes, requestedAudience, &userSession)
|
||||
|
||||
workflowCreated := time.Unix(userSession.OIDCWorkflowSession.CreatedTimestamp, 0)
|
||||
|
||||
userSession.OIDCWorkflowSession = nil
|
||||
|
||||
if err = ctx.SaveSession(userSession); err != nil {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred saving session: %+v", requester.GetID(), client.GetID(), err)
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not save the session."))
|
||||
var (
|
||||
consent *model.OAuth2ConsentSession
|
||||
handled bool
|
||||
)
|
||||
|
||||
if consent, handled = handleOIDCAuthorizationConsent(ctx, issuer, client, userSession, subject, rw, r, requester); handled {
|
||||
return
|
||||
}
|
||||
|
||||
extraClaims := oidcGrantRequests(requester, consent, &userSession)
|
||||
|
||||
if authTime, err = userSession.AuthenticatedTime(client.Policy); err != nil {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred checking authentication time: %+v", requester.GetID(), client.GetID(), err)
|
||||
|
||||
|
@ -100,9 +93,8 @@ func OpenIDConnectAuthorizationGET(ctx *middlewares.AutheliaCtx, rw http.Respons
|
|||
|
||||
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' was successfully processed, proceeding to build Authorization Response", requester.GetID(), clientID)
|
||||
|
||||
subject := userSession.Username
|
||||
oidcSession := oidc.NewSessionWithAuthorizeRequest(issuer, ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID(),
|
||||
subject, userSession.Username, userSession.AuthenticationMethodRefs.MarshalRFC8176(), extraClaims, authTime, workflowCreated, requester)
|
||||
userSession.Username, userSession.AuthenticationMethodRefs.MarshalRFC8176(), extraClaims, authTime, consent, requester)
|
||||
|
||||
ctx.Logger.Tracef("Authorization Request with id '%s' on client with id '%s' creating session for Authorization Response for subject '%s' with username '%s' with claims: %+v",
|
||||
requester.GetID(), oidcSession.ClientID, oidcSession.Subject, oidcSession.Username, oidcSession.Claims)
|
||||
|
@ -119,39 +111,13 @@ func OpenIDConnectAuthorizationGET(ctx *middlewares.AutheliaCtx, rw http.Respons
|
|||
return
|
||||
}
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeResponse(rw, requester, responder)
|
||||
}
|
||||
|
||||
func oidcAuthorizeHandleAuthorizationOrConsentInsufficient(
|
||||
ctx *middlewares.AutheliaCtx, userSession session.UserSession, client *oidc.InternalClient, isAuthInsufficient bool,
|
||||
rw http.ResponseWriter, r *http.Request,
|
||||
requester fosite.AuthorizeRequester, issuer string) {
|
||||
redirectURL := fmt.Sprintf("%s%s", issuer, string(ctx.Request.RequestURI()))
|
||||
|
||||
ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' requires user '%s' provides consent for scopes '%s'",
|
||||
requester.GetID(), client.GetID(), userSession.Username, strings.Join(requester.GetRequestedScopes(), "', '"))
|
||||
|
||||
userSession.OIDCWorkflowSession = &model.OIDCWorkflowSession{
|
||||
ClientID: client.GetID(),
|
||||
RequestedScopes: requester.GetRequestedScopes(),
|
||||
RequestedAudience: requester.GetRequestedAudience(),
|
||||
AuthURI: redirectURL,
|
||||
TargetURI: requester.GetRedirectURI().String(),
|
||||
Require2FA: client.Policy == authorization.TwoFactor,
|
||||
CreatedTimestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := ctx.SaveSession(userSession); err != nil {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred saving session for consent: %+v", requester.GetID(), client.GetID(), err)
|
||||
if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionGranted(ctx, consent.ID); err != nil {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred saving consent session: %+v", requester.GetID(), client.GetID(), err)
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not save the session."))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if isAuthInsufficient {
|
||||
http.Redirect(rw, r, issuer, http.StatusFound)
|
||||
} else {
|
||||
http.Redirect(rw, r, fmt.Sprintf("%s/consent", issuer), http.StatusFound)
|
||||
}
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeResponse(rw, requester, responder)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ory/fosite"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
"github.com/authelia/authelia/v4/internal/oidc"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
"github.com/authelia/authelia/v4/internal/storage"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client,
|
||||
userSession session.UserSession, subject uuid.UUID,
|
||||
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
|
||||
if userSession.ConsentChallengeID != nil {
|
||||
return handleOIDCAuthorizationConsentWithChallengeID(ctx, rootURI, client, userSession, rw, r, requester)
|
||||
}
|
||||
|
||||
return handleOIDCAuthorizationConsentOrGenerate(ctx, rootURI, client, userSession, subject, rw, r, requester)
|
||||
}
|
||||
|
||||
func handleOIDCAuthorizationConsentWithChallengeID(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client,
|
||||
userSession session.UserSession,
|
||||
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, *userSession.ConsentChallengeID); err != nil {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred during consent session lookup: %+v", requester.GetID(), requester.GetClient().GetID(), err)
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Failed to lookup consent session."))
|
||||
|
||||
userSession.ConsentChallengeID = nil
|
||||
|
||||
if err = ctx.SaveSession(userSession); err != nil {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred unlinking consent session challenge id: %+v", requester.GetID(), requester.GetClient().GetID(), err)
|
||||
}
|
||||
|
||||
return nil, true
|
||||
}
|
||||
|
||||
if consent.Responded() {
|
||||
userSession.ConsentChallengeID = nil
|
||||
|
||||
if err = ctx.SaveSession(userSession); err != nil {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred saving session: %+v", requester.GetID(), client.GetID(), err)
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not save the session."))
|
||||
|
||||
return nil, true
|
||||
}
|
||||
|
||||
if consent.Granted {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: this consent session with challenge id '%s' was already granted", requester.GetID(), client.GetID(), consent.ChallengeID.String())
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Authorization already granted."))
|
||||
|
||||
return nil, true
|
||||
}
|
||||
|
||||
ctx.Logger.Debugf("Authorization Request with id '%s' loaded consent session with id '%d' and challenge id '%s' for client id '%s' and subject '%s' and scopes '%s'", requester.GetID(), consent.ID, consent.ChallengeID.String(), client.GetID(), consent.Subject.String(), strings.Join(requester.GetRequestedScopes(), " "))
|
||||
|
||||
if consent.IsDenied() {
|
||||
ctx.Logger.Warnf("Authorization Request with id '%s' and challenge id '%s' for client id '%s' and subject '%s' and scopes '%s' was not denied by the user durng the consent session", requester.GetID(), consent.ChallengeID.String(), client.GetID(), consent.Subject.String(), strings.Join(requester.GetRequestedScopes(), " "))
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrAccessDenied)
|
||||
|
||||
return nil, true
|
||||
}
|
||||
|
||||
return consent, false
|
||||
}
|
||||
|
||||
handleOIDCAuthorizationConsentRedirect(rootURI, client, userSession, rw, r)
|
||||
|
||||
return consent, true
|
||||
}
|
||||
|
||||
func handleOIDCAuthorizationConsentOrGenerate(ctx *middlewares.AutheliaCtx, rootURI string, client *oidc.Client,
|
||||
userSession session.UserSession, subject uuid.UUID,
|
||||
rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) (consent *model.OAuth2ConsentSession, handled bool) {
|
||||
var (
|
||||
rows *storage.ConsentSessionRows
|
||||
scopes, audience []string
|
||||
err error
|
||||
)
|
||||
|
||||
if rows, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionsPreConfigured(ctx, client.GetID(), subject); err != nil {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' had error looking up pre-configured consent sessions: %+v", requester.GetID(), requester.GetClient().GetID(), err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
if consent, err = rows.Get(); err != nil {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' had error looking up pre-configured consent sessions: %+v", requester.GetID(), requester.GetClient().GetID(), err)
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not lookup pre-configured consent sessions."))
|
||||
|
||||
return nil, true
|
||||
}
|
||||
|
||||
scopes, audience = getExpectedScopesAndAudience(requester)
|
||||
|
||||
if consent.HasExactGrants(scopes, audience) && consent.CanGrant() {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if consent != nil && consent.HasExactGrants(scopes, audience) && consent.CanGrant() {
|
||||
return consent, false
|
||||
}
|
||||
|
||||
if consent, err = model.NewOAuth2ConsentSession(subject, requester); err != nil {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred generating consent: %+v", requester.GetID(), requester.GetClient().GetID(), err)
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not generate the consent session."))
|
||||
|
||||
return nil, true
|
||||
}
|
||||
|
||||
if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSession(ctx, *consent); err != nil {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred saving consent session: %+v", requester.GetID(), client.GetID(), err)
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not save the consent session."))
|
||||
|
||||
return nil, true
|
||||
}
|
||||
|
||||
userSession.ConsentChallengeID = &consent.ChallengeID
|
||||
|
||||
if err = ctx.SaveSession(userSession); err != nil {
|
||||
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred saving user session for consent: %+v", requester.GetID(), client.GetID(), err)
|
||||
|
||||
ctx.Providers.OpenIDConnect.Fosite.WriteAuthorizeError(rw, requester, fosite.ErrServerError.WithHint("Could not save the user session."))
|
||||
|
||||
return nil, true
|
||||
}
|
||||
|
||||
handleOIDCAuthorizationConsentRedirect(rootURI, client, userSession, rw, r)
|
||||
|
||||
return consent, true
|
||||
}
|
||||
|
||||
func handleOIDCAuthorizationConsentRedirect(destination string, client *oidc.Client, userSession session.UserSession, rw http.ResponseWriter, r *http.Request) {
|
||||
if client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
|
||||
destination = fmt.Sprintf("%s/consent", destination)
|
||||
}
|
||||
|
||||
http.Redirect(rw, r, destination, http.StatusFound)
|
||||
}
|
||||
|
||||
func getExpectedScopesAndAudience(requester fosite.Requester) (scopes, audience []string) {
|
||||
audience = requester.GetRequestedAudience()
|
||||
if !utils.IsStringInSlice(requester.GetClient().GetID(), audience) {
|
||||
audience = append(audience, requester.GetClient().GetID())
|
||||
}
|
||||
|
||||
return requester.GetRequestedScopes(), audience
|
||||
}
|
|
@ -5,116 +5,135 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
"github.com/authelia/authelia/v4/internal/oidc"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// OpenIDConnectConsentGET handles requests to provide consent for OpenID Connect.
|
||||
func OpenIDConnectConsentGET(ctx *middlewares.AutheliaCtx) {
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
if userSession.OIDCWorkflowSession == nil {
|
||||
ctx.Logger.Debugf("Cannot consent for user %s when OIDC workflow has not been initiated", userSession.Username)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
clientID := userSession.OIDCWorkflowSession.ClientID
|
||||
client, err := ctx.Providers.OpenIDConnect.Store.GetInternalClient(clientID)
|
||||
|
||||
if err != nil {
|
||||
ctx.Logger.Debugf("Unable to find related client configuration with name '%s': %v", clientID, err)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
userSession, consent, client, handled := oidcConsentGetSessionsAndClient(ctx)
|
||||
if handled {
|
||||
return
|
||||
}
|
||||
|
||||
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
|
||||
ctx.Logger.Debugf("Insufficient permissions to give consent during GET current level: %d, require 2FA: %t", userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.Require2FA)
|
||||
ctx.Logger.Errorf("Unable to perform consent without sufficient authentication for user '%s' and client id '%s'", userSession.Username, consent.ClientID)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := ctx.SetJSONBody(client.GetConsentResponseBody(userSession.OIDCWorkflowSession)); err != nil {
|
||||
if err := ctx.SetJSONBody(client.GetConsentResponseBody(consent)); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to set JSON body: %v", err), "Operation failed")
|
||||
}
|
||||
}
|
||||
|
||||
// OpenIDConnectConsentPOST handles consent responses for OpenID Connect.
|
||||
func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
|
||||
userSession := ctx.GetSession()
|
||||
var (
|
||||
body oidc.ConsentPostRequestBody
|
||||
err error
|
||||
)
|
||||
|
||||
if userSession.OIDCWorkflowSession == nil {
|
||||
ctx.Logger.Debugf("Cannot consent for user %s when OIDC workflow has not been initiated", userSession.Username)
|
||||
ctx.ReplyForbidden()
|
||||
if err = json.Unmarshal(ctx.Request.Body(), &body); err != nil {
|
||||
ctx.Logger.Errorf("Failed to parse JSON body in consent POST: %+v", err)
|
||||
ctx.SetJSONError(messageOperationFailed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
client, err := ctx.Providers.OpenIDConnect.Store.GetInternalClient(userSession.OIDCWorkflowSession.ClientID)
|
||||
|
||||
if err != nil {
|
||||
ctx.Logger.Debugf("Unable to find related client configuration with name '%s': %v", userSession.OIDCWorkflowSession.ClientID, err)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
userSession, consent, client, handled := oidcConsentGetSessionsAndClient(ctx)
|
||||
if handled {
|
||||
return
|
||||
}
|
||||
|
||||
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
|
||||
ctx.Logger.Debugf("Insufficient permissions to give consent during POST current level: %d, require 2FA: %t", userSession.AuthenticationLevel, userSession.OIDCWorkflowSession.Require2FA)
|
||||
ctx.Logger.Debugf("Insufficient permissions to give consent during POST current level: %d, require 2FA: %d", userSession.AuthenticationLevel, client.Policy)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var body ConsentPostRequestBody
|
||||
err = json.Unmarshal(ctx.Request.Body(), &body)
|
||||
if consent.ClientID != body.ClientID {
|
||||
ctx.Logger.Errorf("User '%s' consented to scopes of another client (%s) than expected (%s). Beware this can be a sign of attack",
|
||||
userSession.Username, body.ClientID, consent.ClientID)
|
||||
ctx.SetJSONError(messageOperationFailed)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to unmarshal body: %v", err), "Operation failed")
|
||||
return
|
||||
}
|
||||
|
||||
if body.AcceptOrReject != accept && body.AcceptOrReject != reject {
|
||||
ctx.Logger.Infof("User %s tried to reply to consent with an unexpected verb", userSession.Username)
|
||||
var (
|
||||
externalRootURL string
|
||||
authorized = true
|
||||
)
|
||||
|
||||
switch body.AcceptOrReject {
|
||||
case accept:
|
||||
if externalRootURL, err = ctx.ExternalRootURL(); err != nil {
|
||||
ctx.Logger.Errorf("Could not determine the external URL during consent session processing with challenge id '%s' for user '%s': %v", consent.ChallengeID.String(), userSession.Username, err)
|
||||
ctx.SetJSONError(messageOperationFailed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
consent.GrantedScopes = consent.RequestedScopes
|
||||
consent.GrantedAudience = consent.RequestedAudience
|
||||
|
||||
if !utils.IsStringInSlice(consent.ClientID, consent.GrantedAudience) {
|
||||
consent.GrantedAudience = append(consent.GrantedAudience, consent.ClientID)
|
||||
}
|
||||
case reject:
|
||||
authorized = false
|
||||
default:
|
||||
ctx.Logger.Warnf("User '%s' tried to reply to consent with an unexpected verb", userSession.Username)
|
||||
ctx.ReplyBadRequest()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if userSession.OIDCWorkflowSession.ClientID != body.ClientID {
|
||||
ctx.Logger.Infof("User %s consented to scopes of another client (%s) than expected (%s). Beware this can be a sign of attack",
|
||||
userSession.Username, body.ClientID, userSession.OIDCWorkflowSession.ClientID)
|
||||
ctx.ReplyBadRequest()
|
||||
if err = ctx.Providers.StorageProvider.SaveOAuth2ConsentSessionResponse(ctx, *consent, authorized); err != nil {
|
||||
ctx.Logger.Errorf("Failed to save the consent session response to the database: %+v", err)
|
||||
ctx.SetJSONError(messageOperationFailed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var redirectionURL string
|
||||
response := oidc.ConsentPostResponseBody{RedirectURI: fmt.Sprintf("%s%s?%s", externalRootURL, oidc.AuthorizationPath, consent.Form)}
|
||||
|
||||
if body.AcceptOrReject == accept {
|
||||
redirectionURL = userSession.OIDCWorkflowSession.AuthURI
|
||||
userSession.OIDCWorkflowSession.GrantedScopes = userSession.OIDCWorkflowSession.RequestedScopes
|
||||
userSession.OIDCWorkflowSession.GrantedAudience = userSession.OIDCWorkflowSession.RequestedAudience
|
||||
|
||||
if err := ctx.SaveSession(userSession); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to write session: %v", err), "Operation failed")
|
||||
return
|
||||
}
|
||||
} else if body.AcceptOrReject == reject {
|
||||
redirectionURL = fmt.Sprintf("%s?error=access_denied&error_description=%s",
|
||||
userSession.OIDCWorkflowSession.TargetURI, "User has rejected the scopes")
|
||||
userSession.OIDCWorkflowSession = nil
|
||||
|
||||
if err := ctx.SaveSession(userSession); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to write session: %v", err), "Operation failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response := ConsentPostResponseBody{RedirectURI: redirectionURL}
|
||||
|
||||
if err := ctx.SetJSONBody(response); err != nil {
|
||||
if err = ctx.SetJSONBody(response); err != nil {
|
||||
ctx.Error(fmt.Errorf("unable to set JSON body in response"), "Operation failed")
|
||||
}
|
||||
}
|
||||
|
||||
func oidcConsentGetSessionsAndClient(ctx *middlewares.AutheliaCtx) (userSession session.UserSession, consent *model.OAuth2ConsentSession, client *oidc.Client, handled bool) {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
userSession = ctx.GetSession()
|
||||
|
||||
if userSession.ConsentChallengeID == nil {
|
||||
ctx.Logger.Errorf("Cannot consent for user '%s' when OIDC consent session has not been initiated", userSession.Username)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return userSession, nil, nil, true
|
||||
}
|
||||
|
||||
if consent, err = ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, *userSession.ConsentChallengeID); err != nil {
|
||||
ctx.Logger.Errorf("Unable to load consent session with challenge id '%s': %v", userSession.ConsentChallengeID.String(), err)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return userSession, nil, nil, true
|
||||
}
|
||||
|
||||
if client, err = ctx.Providers.OpenIDConnect.Store.GetFullClient(consent.ClientID); err != nil {
|
||||
ctx.Logger.Errorf("Unable to find related client configuration with name '%s': %v", consent.ClientID, err)
|
||||
ctx.ReplyForbidden()
|
||||
|
||||
return userSession, nil, nil, true
|
||||
}
|
||||
|
||||
return userSession, consent, client, false
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
"github.com/authelia/authelia/v4/internal/oidc"
|
||||
)
|
||||
|
||||
|
@ -21,7 +22,7 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter,
|
|||
var (
|
||||
tokenType fosite.TokenType
|
||||
requester fosite.AccessRequester
|
||||
client *oidc.InternalClient
|
||||
client *oidc.Client
|
||||
err error
|
||||
)
|
||||
|
||||
|
@ -54,13 +55,13 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter,
|
|||
return
|
||||
}
|
||||
|
||||
if client, err = ctx.Providers.OpenIDConnect.Store.GetInternalClient(clientID); err != nil {
|
||||
if client, err = ctx.Providers.OpenIDConnect.Store.GetFullClient(clientID); err != nil {
|
||||
ctx.Providers.OpenIDConnect.WriteError(rw, req, errors.WithStack(fosite.ErrServerError.WithHint("Unable to assert type of client")))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
claims := requester.GetSession().(*oidc.OpenIDSession).IDTokenClaims().ToMap()
|
||||
claims := requester.GetSession().(*model.OpenIDSession).IDTokenClaims().ToMap()
|
||||
delete(claims, "jti")
|
||||
delete(claims, "sid")
|
||||
delete(claims, "at_hash")
|
||||
|
|
|
@ -266,7 +266,7 @@ func HandleAllow(ctx *middlewares.AutheliaCtx, targetURL string) {
|
|||
return
|
||||
}
|
||||
|
||||
if userSession.OIDCWorkflowSession != nil {
|
||||
if userSession.ConsentChallengeID != nil {
|
||||
handleOIDCWorkflowResponse(ctx)
|
||||
} else {
|
||||
Handle2FAResponse(ctx, targetURL)
|
||||
|
|
|
@ -78,7 +78,7 @@ func SecondFactorTOTPPost(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
if userSession.OIDCWorkflowSession != nil {
|
||||
if userSession.ConsentChallengeID != nil {
|
||||
handleOIDCWorkflowResponse(ctx)
|
||||
} else {
|
||||
Handle2FAResponse(ctx, requestBody.TargetURL)
|
||||
|
|
|
@ -197,7 +197,7 @@ func SecondFactorWebauthnAssertionPOST(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
if userSession.OIDCWorkflowSession != nil {
|
||||
if userSession.ConsentChallengeID != nil {
|
||||
handleOIDCWorkflowResponse(ctx)
|
||||
} else {
|
||||
Handle2FAResponse(ctx, requestBody.TargetURL)
|
||||
|
|
|
@ -6,24 +6,12 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/model"
|
||||
"github.com/authelia/authelia/v4/internal/oidc"
|
||||
"github.com/authelia/authelia/v4/internal/session"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// isConsentMissing compares the requestedScopes and requestedAudience to the workflows
|
||||
// GrantedScopes and GrantedAudience and returns true if they do not match or the workflow is nil.
|
||||
func isConsentMissing(workflow *model.OIDCWorkflowSession, requestedScopes, requestedAudience []string) (isMissing bool) {
|
||||
if workflow == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return len(requestedScopes) > 0 && utils.IsStringSlicesDifferent(requestedScopes, workflow.GrantedScopes) ||
|
||||
len(requestedAudience) > 0 && utils.IsStringSlicesDifferentFold(requestedAudience, workflow.GrantedAudience)
|
||||
}
|
||||
|
||||
func oidcGrantRequests(ar fosite.AuthorizeRequester, scopes, audiences []string, userSession *session.UserSession) (extraClaims map[string]interface{}) {
|
||||
func oidcGrantRequests(ar fosite.AuthorizeRequester, consent *model.OAuth2ConsentSession, userSession *session.UserSession) (extraClaims map[string]interface{}) {
|
||||
extraClaims = map[string]interface{}{}
|
||||
|
||||
for _, scope := range scopes {
|
||||
for _, scope := range consent.GrantedScopes {
|
||||
if ar != nil {
|
||||
ar.GrantScope(scope)
|
||||
}
|
||||
|
@ -47,13 +35,9 @@ func oidcGrantRequests(ar fosite.AuthorizeRequester, scopes, audiences []string,
|
|||
}
|
||||
|
||||
if ar != nil {
|
||||
for _, audience := range audiences {
|
||||
for _, audience := range consent.GrantedAudience {
|
||||
ar.GrantAudience(audience)
|
||||
}
|
||||
|
||||
if !utils.IsStringInSlice(ar.GetClient().GetID(), ar.GetGrantedAudience()) {
|
||||
ar.GrantAudience(ar.GetClient().GetID())
|
||||
}
|
||||
}
|
||||
|
||||
return extraClaims
|
||||
|
|
|
@ -11,32 +11,12 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/session"
|
||||
)
|
||||
|
||||
func TestShouldDetectIfConsentIsMissing(t *testing.T) {
|
||||
var workflow *model.OIDCWorkflowSession
|
||||
|
||||
requestedScopes := []string{"openid", "profile"}
|
||||
requestedAudience := []string{"https://authelia.com"}
|
||||
|
||||
assert.True(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
|
||||
|
||||
workflow = &model.OIDCWorkflowSession{
|
||||
GrantedScopes: []string{"openid", "profile"},
|
||||
GrantedAudience: []string{"https://authelia.com"},
|
||||
func TestShouldGrantAppropriateClaimsForScopeProfile(t *testing.T) {
|
||||
consent := &model.OAuth2ConsentSession{
|
||||
GrantedScopes: []string{oidc.ScopeProfile},
|
||||
}
|
||||
|
||||
assert.False(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
|
||||
|
||||
requestedScopes = []string{"openid", "profile", "group"}
|
||||
|
||||
assert.True(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
|
||||
|
||||
requestedScopes = []string{"openid", "profile"}
|
||||
requestedAudience = []string{"https://not.authelia.com"}
|
||||
assert.True(t, isConsentMissing(workflow, requestedScopes, requestedAudience))
|
||||
}
|
||||
|
||||
func TestShouldGrantAppropriateClaimsForScopeProfile(t *testing.T) {
|
||||
extraClaims := oidcGrantRequests(nil, []string{oidc.ScopeProfile}, []string{}, &oidcUserSessionJohn)
|
||||
extraClaims := oidcGrantRequests(nil, consent, &oidcUserSessionJohn)
|
||||
|
||||
assert.Len(t, extraClaims, 2)
|
||||
|
||||
|
@ -48,7 +28,11 @@ func TestShouldGrantAppropriateClaimsForScopeProfile(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestShouldGrantAppropriateClaimsForScopeGroups(t *testing.T) {
|
||||
extraClaims := oidcGrantRequests(nil, []string{oidc.ScopeGroups}, []string{}, &oidcUserSessionJohn)
|
||||
consent := &model.OAuth2ConsentSession{
|
||||
GrantedScopes: []string{oidc.ScopeGroups},
|
||||
}
|
||||
|
||||
extraClaims := oidcGrantRequests(nil, consent, &oidcUserSessionJohn)
|
||||
|
||||
assert.Len(t, extraClaims, 1)
|
||||
|
||||
|
@ -57,7 +41,7 @@ func TestShouldGrantAppropriateClaimsForScopeGroups(t *testing.T) {
|
|||
assert.Contains(t, extraClaims[oidc.ClaimGroups], "admin")
|
||||
assert.Contains(t, extraClaims[oidc.ClaimGroups], "dev")
|
||||
|
||||
extraClaims = oidcGrantRequests(nil, []string{oidc.ScopeGroups}, []string{}, &oidcUserSessionFred)
|
||||
extraClaims = oidcGrantRequests(nil, consent, &oidcUserSessionFred)
|
||||
|
||||
assert.Len(t, extraClaims, 1)
|
||||
|
||||
|
@ -67,7 +51,11 @@ func TestShouldGrantAppropriateClaimsForScopeGroups(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestShouldGrantAppropriateClaimsForScopeEmail(t *testing.T) {
|
||||
extraClaims := oidcGrantRequests(nil, []string{oidc.ScopeEmail}, []string{}, &oidcUserSessionJohn)
|
||||
consent := &model.OAuth2ConsentSession{
|
||||
GrantedScopes: []string{oidc.ScopeEmail},
|
||||
}
|
||||
|
||||
extraClaims := oidcGrantRequests(nil, consent, &oidcUserSessionJohn)
|
||||
|
||||
assert.Len(t, extraClaims, 3)
|
||||
|
||||
|
@ -81,7 +69,7 @@ func TestShouldGrantAppropriateClaimsForScopeEmail(t *testing.T) {
|
|||
require.Contains(t, extraClaims, oidc.ClaimEmailVerified)
|
||||
assert.Equal(t, true, extraClaims[oidc.ClaimEmailVerified])
|
||||
|
||||
extraClaims = oidcGrantRequests(nil, []string{oidc.ScopeEmail}, []string{}, &oidcUserSessionFred)
|
||||
extraClaims = oidcGrantRequests(nil, consent, &oidcUserSessionFred)
|
||||
|
||||
assert.Len(t, extraClaims, 2)
|
||||
|
||||
|
@ -93,7 +81,11 @@ func TestShouldGrantAppropriateClaimsForScopeEmail(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestShouldGrantAppropriateClaimsForScopeOpenIDAndProfile(t *testing.T) {
|
||||
extraClaims := oidcGrantRequests(nil, []string{oidc.ScopeOpenID, oidc.ScopeProfile}, []string{}, &oidcUserSessionJohn)
|
||||
consent := &model.OAuth2ConsentSession{
|
||||
GrantedScopes: []string{oidc.ScopeOpenID, oidc.ScopeProfile},
|
||||
}
|
||||
|
||||
extraClaims := oidcGrantRequests(nil, consent, &oidcUserSessionJohn)
|
||||
|
||||
assert.Len(t, extraClaims, 2)
|
||||
|
||||
|
@ -103,7 +95,7 @@ func TestShouldGrantAppropriateClaimsForScopeOpenIDAndProfile(t *testing.T) {
|
|||
require.Contains(t, extraClaims, oidc.ClaimDisplayName)
|
||||
assert.Equal(t, "John Smith", extraClaims[oidc.ClaimDisplayName])
|
||||
|
||||
extraClaims = oidcGrantRequests(nil, []string{oidc.ScopeOpenID, oidc.ScopeProfile}, []string{}, &oidcUserSessionFred)
|
||||
extraClaims = oidcGrantRequests(nil, consent, &oidcUserSessionFred)
|
||||
|
||||
assert.Len(t, extraClaims, 2)
|
||||
|
||||
|
|
|
@ -7,9 +7,9 @@ import (
|
|||
|
||||
"github.com/valyala/fasthttp"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authentication"
|
||||
"github.com/authelia/authelia/v4/internal/authorization"
|
||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||
"github.com/authelia/authelia/v4/internal/oidc"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
|
@ -17,14 +17,15 @@ import (
|
|||
func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) {
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
if userSession.OIDCWorkflowSession.Require2FA && userSession.AuthenticationLevel != authentication.TwoFactor {
|
||||
ctx.Logger.Warnf("OpenID Connect client '%s' requires 2FA, cannot be redirected yet", userSession.OIDCWorkflowSession.ClientID)
|
||||
ctx.ReplyOK()
|
||||
if userSession.ConsentChallengeID == nil {
|
||||
ctx.Logger.Errorf("Unable to handle OIDC workflow response because the user session doesn't contain a consent challenge id")
|
||||
|
||||
respondUnauthorized(ctx, messageOperationFailed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
uri, err := ctx.ExternalRootURL()
|
||||
externalRootURL, err := ctx.ExternalRootURL()
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("Unable to determine external Base URL: %v", err)
|
||||
|
||||
|
@ -33,18 +34,37 @@ func handleOIDCWorkflowResponse(ctx *middlewares.AutheliaCtx) {
|
|||
return
|
||||
}
|
||||
|
||||
if isConsentMissing(
|
||||
userSession.OIDCWorkflowSession,
|
||||
userSession.OIDCWorkflowSession.RequestedScopes,
|
||||
userSession.OIDCWorkflowSession.RequestedAudience) {
|
||||
err = ctx.SetJSONBody(redirectResponse{Redirect: fmt.Sprintf("%s/consent", uri)})
|
||||
|
||||
consent, err := ctx.Providers.StorageProvider.LoadOAuth2ConsentSessionByChallengeID(ctx, *userSession.ConsentChallengeID)
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("Unable to load consent session from database: %v", err)
|
||||
|
||||
respondUnauthorized(ctx, messageOperationFailed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
client, err := ctx.Providers.OpenIDConnect.Store.GetFullClient(consent.ClientID)
|
||||
if err != nil {
|
||||
ctx.Logger.Errorf("Unable to find client for the consent session: %v", err)
|
||||
|
||||
respondUnauthorized(ctx, messageOperationFailed)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) {
|
||||
ctx.Logger.Warnf("OpenID Connect client '%s' requires 2FA, cannot be redirected yet", client.ID)
|
||||
ctx.ReplyOK()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if userSession.ConsentChallengeID != nil {
|
||||
if err = ctx.SetJSONBody(redirectResponse{Redirect: fmt.Sprintf("%s/consent", externalRootURL)}); err != nil {
|
||||
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
|
||||
}
|
||||
} else {
|
||||
err = ctx.SetJSONBody(redirectResponse{Redirect: userSession.OIDCWorkflowSession.AuthURI})
|
||||
if err != nil {
|
||||
if err = ctx.SetJSONBody(redirectResponse{Redirect: fmt.Sprintf("%s%s?%s", externalRootURL, oidc.AuthorizationPath, consent.Form)}); err != nil {
|
||||
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
package handlers
|
||||
|
||||
// ConsentPostRequestBody schema of the request body of the consent POST endpoint.
|
||||
type ConsentPostRequestBody struct {
|
||||
ClientID string `json:"client_id"`
|
||||
AcceptOrReject string `json:"accept_or_reject"`
|
||||
}
|
||||
|
||||
// ConsentPostResponseBody schema of the response body of the consent POST endpoint.
|
||||
type ConsentPostResponseBody struct {
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
}
|
|
@ -10,8 +10,10 @@ import (
|
|||
time "time"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
uuid "github.com/google/uuid"
|
||||
|
||||
model "github.com/authelia/authelia/v4/internal/model"
|
||||
storage "github.com/authelia/authelia/v4/internal/storage"
|
||||
)
|
||||
|
||||
// MockStorage is a mock of Provider interface.
|
||||
|
@ -51,6 +53,21 @@ func (mr *MockStorageMockRecorder) AppendAuthenticationLog(arg0, arg1 interface{
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AppendAuthenticationLog", reflect.TypeOf((*MockStorage)(nil).AppendAuthenticationLog), arg0, arg1)
|
||||
}
|
||||
|
||||
// BeginTX mocks base method.
|
||||
func (m *MockStorage) BeginTX(arg0 context.Context) (context.Context, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "BeginTX", arg0)
|
||||
ret0, _ := ret[0].(context.Context)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// BeginTX indicates an expected call of BeginTX.
|
||||
func (mr *MockStorageMockRecorder) BeginTX(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginTX", reflect.TypeOf((*MockStorage)(nil).BeginTX), arg0)
|
||||
}
|
||||
|
||||
// Close mocks base method.
|
||||
func (m *MockStorage) Close() error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -65,6 +82,20 @@ func (mr *MockStorageMockRecorder) Close() *gomock.Call {
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockStorage)(nil).Close))
|
||||
}
|
||||
|
||||
// Commit mocks base method.
|
||||
func (m *MockStorage) Commit(arg0 context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Commit", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Commit indicates an expected call of Commit.
|
||||
func (mr *MockStorageMockRecorder) Commit(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockStorage)(nil).Commit), arg0)
|
||||
}
|
||||
|
||||
// ConsumeIdentityVerification mocks base method.
|
||||
func (m *MockStorage) ConsumeIdentityVerification(arg0 context.Context, arg1 string, arg2 model.NullIP) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -79,6 +110,34 @@ func (mr *MockStorageMockRecorder) ConsumeIdentityVerification(arg0, arg1, arg2
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConsumeIdentityVerification", reflect.TypeOf((*MockStorage)(nil).ConsumeIdentityVerification), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// DeactivateOAuth2Session mocks base method.
|
||||
func (m *MockStorage) DeactivateOAuth2Session(arg0 context.Context, arg1 storage.OAuth2SessionType, arg2 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeactivateOAuth2Session", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeactivateOAuth2Session indicates an expected call of DeactivateOAuth2Session.
|
||||
func (mr *MockStorageMockRecorder) DeactivateOAuth2Session(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeactivateOAuth2Session", reflect.TypeOf((*MockStorage)(nil).DeactivateOAuth2Session), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// DeactivateOAuth2SessionByRequestID mocks base method.
|
||||
func (m *MockStorage) DeactivateOAuth2SessionByRequestID(arg0 context.Context, arg1 storage.OAuth2SessionType, arg2 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeactivateOAuth2SessionByRequestID", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeactivateOAuth2SessionByRequestID indicates an expected call of DeactivateOAuth2SessionByRequestID.
|
||||
func (mr *MockStorageMockRecorder) DeactivateOAuth2SessionByRequestID(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeactivateOAuth2SessionByRequestID", reflect.TypeOf((*MockStorage)(nil).DeactivateOAuth2SessionByRequestID), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// DeletePreferredDuoDevice mocks base method.
|
||||
func (m *MockStorage) DeletePreferredDuoDevice(arg0 context.Context, arg1 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -137,6 +196,66 @@ func (mr *MockStorageMockRecorder) LoadAuthenticationLogs(arg0, arg1, arg2, arg3
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadAuthenticationLogs", reflect.TypeOf((*MockStorage)(nil).LoadAuthenticationLogs), arg0, arg1, arg2, arg3, arg4)
|
||||
}
|
||||
|
||||
// LoadOAuth2BlacklistedJTI mocks base method.
|
||||
func (m *MockStorage) LoadOAuth2BlacklistedJTI(arg0 context.Context, arg1 string) (*model.OAuth2BlacklistedJTI, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LoadOAuth2BlacklistedJTI", arg0, arg1)
|
||||
ret0, _ := ret[0].(*model.OAuth2BlacklistedJTI)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LoadOAuth2BlacklistedJTI indicates an expected call of LoadOAuth2BlacklistedJTI.
|
||||
func (mr *MockStorageMockRecorder) LoadOAuth2BlacklistedJTI(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOAuth2BlacklistedJTI", reflect.TypeOf((*MockStorage)(nil).LoadOAuth2BlacklistedJTI), arg0, arg1)
|
||||
}
|
||||
|
||||
// LoadOAuth2ConsentSessionByChallengeID mocks base method.
|
||||
func (m *MockStorage) LoadOAuth2ConsentSessionByChallengeID(arg0 context.Context, arg1 uuid.UUID) (*model.OAuth2ConsentSession, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LoadOAuth2ConsentSessionByChallengeID", arg0, arg1)
|
||||
ret0, _ := ret[0].(*model.OAuth2ConsentSession)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LoadOAuth2ConsentSessionByChallengeID indicates an expected call of LoadOAuth2ConsentSessionByChallengeID.
|
||||
func (mr *MockStorageMockRecorder) LoadOAuth2ConsentSessionByChallengeID(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOAuth2ConsentSessionByChallengeID", reflect.TypeOf((*MockStorage)(nil).LoadOAuth2ConsentSessionByChallengeID), arg0, arg1)
|
||||
}
|
||||
|
||||
// LoadOAuth2ConsentSessionsPreConfigured mocks base method.
|
||||
func (m *MockStorage) LoadOAuth2ConsentSessionsPreConfigured(arg0 context.Context, arg1 string, arg2 uuid.UUID) (*storage.ConsentSessionRows, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LoadOAuth2ConsentSessionsPreConfigured", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*storage.ConsentSessionRows)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LoadOAuth2ConsentSessionsPreConfigured indicates an expected call of LoadOAuth2ConsentSessionsPreConfigured.
|
||||
func (mr *MockStorageMockRecorder) LoadOAuth2ConsentSessionsPreConfigured(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOAuth2ConsentSessionsPreConfigured", reflect.TypeOf((*MockStorage)(nil).LoadOAuth2ConsentSessionsPreConfigured), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// LoadOAuth2Session mocks base method.
|
||||
func (m *MockStorage) LoadOAuth2Session(arg0 context.Context, arg1 storage.OAuth2SessionType, arg2 string) (*model.OAuth2Session, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LoadOAuth2Session", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*model.OAuth2Session)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LoadOAuth2Session indicates an expected call of LoadOAuth2Session.
|
||||
func (mr *MockStorageMockRecorder) LoadOAuth2Session(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadOAuth2Session", reflect.TypeOf((*MockStorage)(nil).LoadOAuth2Session), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// LoadPreferred2FAMethod mocks base method.
|
||||
func (m *MockStorage) LoadPreferred2FAMethod(arg0 context.Context, arg1 string) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -212,6 +331,36 @@ func (mr *MockStorageMockRecorder) LoadUserInfo(arg0, arg1 interface{}) *gomock.
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserInfo", reflect.TypeOf((*MockStorage)(nil).LoadUserInfo), arg0, arg1)
|
||||
}
|
||||
|
||||
// LoadUserOpaqueIdentifier mocks base method.
|
||||
func (m *MockStorage) LoadUserOpaqueIdentifier(arg0 context.Context, arg1 uuid.UUID) (*model.UserOpaqueIdentifier, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LoadUserOpaqueIdentifier", arg0, arg1)
|
||||
ret0, _ := ret[0].(*model.UserOpaqueIdentifier)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LoadUserOpaqueIdentifier indicates an expected call of LoadUserOpaqueIdentifier.
|
||||
func (mr *MockStorageMockRecorder) LoadUserOpaqueIdentifier(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserOpaqueIdentifier", reflect.TypeOf((*MockStorage)(nil).LoadUserOpaqueIdentifier), arg0, arg1)
|
||||
}
|
||||
|
||||
// LoadUserOpaqueIdentifierBySignature mocks base method.
|
||||
func (m *MockStorage) LoadUserOpaqueIdentifierBySignature(arg0 context.Context, arg1, arg2, arg3 string) (*model.UserOpaqueIdentifier, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "LoadUserOpaqueIdentifierBySignature", arg0, arg1, arg2, arg3)
|
||||
ret0, _ := ret[0].(*model.UserOpaqueIdentifier)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// LoadUserOpaqueIdentifierBySignature indicates an expected call of LoadUserOpaqueIdentifierBySignature.
|
||||
func (mr *MockStorageMockRecorder) LoadUserOpaqueIdentifierBySignature(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadUserOpaqueIdentifierBySignature", reflect.TypeOf((*MockStorage)(nil).LoadUserOpaqueIdentifierBySignature), arg0, arg1, arg2, arg3)
|
||||
}
|
||||
|
||||
// LoadWebauthnDevices mocks base method.
|
||||
func (m *MockStorage) LoadWebauthnDevices(arg0 context.Context, arg1, arg2 int) ([]model.WebauthnDevice, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -242,6 +391,48 @@ func (mr *MockStorageMockRecorder) LoadWebauthnDevicesByUsername(arg0, arg1 inte
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadWebauthnDevicesByUsername", reflect.TypeOf((*MockStorage)(nil).LoadWebauthnDevicesByUsername), arg0, arg1)
|
||||
}
|
||||
|
||||
// RevokeOAuth2Session mocks base method.
|
||||
func (m *MockStorage) RevokeOAuth2Session(arg0 context.Context, arg1 storage.OAuth2SessionType, arg2 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RevokeOAuth2Session", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RevokeOAuth2Session indicates an expected call of RevokeOAuth2Session.
|
||||
func (mr *MockStorageMockRecorder) RevokeOAuth2Session(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeOAuth2Session", reflect.TypeOf((*MockStorage)(nil).RevokeOAuth2Session), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// RevokeOAuth2SessionByRequestID mocks base method.
|
||||
func (m *MockStorage) RevokeOAuth2SessionByRequestID(arg0 context.Context, arg1 storage.OAuth2SessionType, arg2 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "RevokeOAuth2SessionByRequestID", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// RevokeOAuth2SessionByRequestID indicates an expected call of RevokeOAuth2SessionByRequestID.
|
||||
func (mr *MockStorageMockRecorder) RevokeOAuth2SessionByRequestID(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeOAuth2SessionByRequestID", reflect.TypeOf((*MockStorage)(nil).RevokeOAuth2SessionByRequestID), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// Rollback mocks base method.
|
||||
func (m *MockStorage) Rollback(arg0 context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Rollback", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Rollback indicates an expected call of Rollback.
|
||||
func (mr *MockStorageMockRecorder) Rollback(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*MockStorage)(nil).Rollback), arg0)
|
||||
}
|
||||
|
||||
// SaveIdentityVerification mocks base method.
|
||||
func (m *MockStorage) SaveIdentityVerification(arg0 context.Context, arg1 model.IdentityVerification) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -256,6 +447,76 @@ func (mr *MockStorageMockRecorder) SaveIdentityVerification(arg0, arg1 interface
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveIdentityVerification", reflect.TypeOf((*MockStorage)(nil).SaveIdentityVerification), arg0, arg1)
|
||||
}
|
||||
|
||||
// SaveOAuth2BlacklistedJTI mocks base method.
|
||||
func (m *MockStorage) SaveOAuth2BlacklistedJTI(arg0 context.Context, arg1 model.OAuth2BlacklistedJTI) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SaveOAuth2BlacklistedJTI", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SaveOAuth2BlacklistedJTI indicates an expected call of SaveOAuth2BlacklistedJTI.
|
||||
func (mr *MockStorageMockRecorder) SaveOAuth2BlacklistedJTI(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveOAuth2BlacklistedJTI", reflect.TypeOf((*MockStorage)(nil).SaveOAuth2BlacklistedJTI), arg0, arg1)
|
||||
}
|
||||
|
||||
// SaveOAuth2ConsentSession mocks base method.
|
||||
func (m *MockStorage) SaveOAuth2ConsentSession(arg0 context.Context, arg1 model.OAuth2ConsentSession) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SaveOAuth2ConsentSession", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SaveOAuth2ConsentSession indicates an expected call of SaveOAuth2ConsentSession.
|
||||
func (mr *MockStorageMockRecorder) SaveOAuth2ConsentSession(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveOAuth2ConsentSession", reflect.TypeOf((*MockStorage)(nil).SaveOAuth2ConsentSession), arg0, arg1)
|
||||
}
|
||||
|
||||
// SaveOAuth2ConsentSessionGranted mocks base method.
|
||||
func (m *MockStorage) SaveOAuth2ConsentSessionGranted(arg0 context.Context, arg1 int) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SaveOAuth2ConsentSessionGranted", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SaveOAuth2ConsentSessionGranted indicates an expected call of SaveOAuth2ConsentSessionGranted.
|
||||
func (mr *MockStorageMockRecorder) SaveOAuth2ConsentSessionGranted(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveOAuth2ConsentSessionGranted", reflect.TypeOf((*MockStorage)(nil).SaveOAuth2ConsentSessionGranted), arg0, arg1)
|
||||
}
|
||||
|
||||
// SaveOAuth2ConsentSessionResponse mocks base method.
|
||||
func (m *MockStorage) SaveOAuth2ConsentSessionResponse(arg0 context.Context, arg1 model.OAuth2ConsentSession, arg2 bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SaveOAuth2ConsentSessionResponse", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SaveOAuth2ConsentSessionResponse indicates an expected call of SaveOAuth2ConsentSessionResponse.
|
||||
func (mr *MockStorageMockRecorder) SaveOAuth2ConsentSessionResponse(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveOAuth2ConsentSessionResponse", reflect.TypeOf((*MockStorage)(nil).SaveOAuth2ConsentSessionResponse), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// SaveOAuth2Session mocks base method.
|
||||
func (m *MockStorage) SaveOAuth2Session(arg0 context.Context, arg1 storage.OAuth2SessionType, arg2 model.OAuth2Session) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SaveOAuth2Session", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SaveOAuth2Session indicates an expected call of SaveOAuth2Session.
|
||||
func (mr *MockStorageMockRecorder) SaveOAuth2Session(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveOAuth2Session", reflect.TypeOf((*MockStorage)(nil).SaveOAuth2Session), arg0, arg1, arg2)
|
||||
}
|
||||
|
||||
// SavePreferred2FAMethod mocks base method.
|
||||
func (m *MockStorage) SavePreferred2FAMethod(arg0 context.Context, arg1, arg2 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
@ -298,6 +559,20 @@ func (mr *MockStorageMockRecorder) SaveTOTPConfiguration(arg0, arg1 interface{})
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTOTPConfiguration", reflect.TypeOf((*MockStorage)(nil).SaveTOTPConfiguration), arg0, arg1)
|
||||
}
|
||||
|
||||
// SaveUserOpaqueIdentifier mocks base method.
|
||||
func (m *MockStorage) SaveUserOpaqueIdentifier(arg0 context.Context, arg1 model.UserOpaqueIdentifier) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "SaveUserOpaqueIdentifier", arg0, arg1)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// SaveUserOpaqueIdentifier indicates an expected call of SaveUserOpaqueIdentifier.
|
||||
func (mr *MockStorageMockRecorder) SaveUserOpaqueIdentifier(arg0, arg1 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveUserOpaqueIdentifier", reflect.TypeOf((*MockStorage)(nil).SaveUserOpaqueIdentifier), arg0, arg1)
|
||||
}
|
||||
|
||||
// SaveWebauthnDevice mocks base method.
|
||||
func (m *MockStorage) SaveWebauthnDevice(arg0 context.Context, arg1 model.WebauthnDevice) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -1,14 +1,228 @@
|
|||
package model
|
||||
|
||||
// OIDCWorkflowSession represent an OIDC workflow session.
|
||||
type OIDCWorkflowSession struct {
|
||||
ClientID string
|
||||
RequestedScopes []string
|
||||
GrantedScopes []string
|
||||
RequestedAudience []string
|
||||
GrantedAudience []string
|
||||
TargetURI string
|
||||
AuthURI string
|
||||
Require2FA bool
|
||||
CreatedTimestamp int64
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// NewIP easily constructs a new IP.
|
||||
|
@ -150,3 +153,26 @@ func (b *Base64) Scan(src interface{}) (err error) {
|
|||
type StartupCheck interface {
|
||||
StartupCheck() (err error)
|
||||
}
|
||||
|
||||
// StringSlicePipeDelimited is a string slice that is stored in the database delimited by pipes.
|
||||
type StringSlicePipeDelimited []string
|
||||
|
||||
// Scan is the StringSlicePipeDelimited implementation of the sql.Scanner.
|
||||
func (s *StringSlicePipeDelimited) Scan(value interface{}) (err error) {
|
||||
var nullStr sql.NullString
|
||||
|
||||
if err = nullStr.Scan(value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if nullStr.Valid {
|
||||
*s = utils.StringSplitDelimitedEscaped(nullStr.String, '|')
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value is the StringSlicePipeDelimited implementation of the databases/sql driver.Valuer.
|
||||
func (s StringSlicePipeDelimited) Value() (driver.Value, error) {
|
||||
return utils.StringJoinDelimitedEscaped(s, '|'), nil
|
||||
}
|
||||
|
|
|
@ -1,11 +1,21 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/fosite"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
args := fosite.Arguments{"abc", "123"}
|
||||
|
||||
x := StringSlicePipeDelimited(args)
|
||||
|
||||
fmt.Println(x)
|
||||
}
|
||||
|
||||
func TestDatabaseModelTypeIP(t *testing.T) {
|
||||
ip := IP{}
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// NewUserOpaqueIdentifier either creates a new UserOpaqueIdentifier or returns an error.
|
||||
func NewUserOpaqueIdentifier(service, sectorID, username string) (id *UserOpaqueIdentifier, err error) {
|
||||
var opaqueID uuid.UUID
|
||||
|
||||
if opaqueID, err = uuid.NewRandom(); err != nil {
|
||||
return nil, fmt.Errorf("unable to generate uuid: %w", err)
|
||||
}
|
||||
|
||||
return &UserOpaqueIdentifier{
|
||||
Service: service,
|
||||
SectorID: sectorID,
|
||||
Username: username,
|
||||
Identifier: opaqueID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UserOpaqueIdentifier represents an opaque identifier for a user. Commonly used with OAuth 2.0 and OpenID Connect.
|
||||
type UserOpaqueIdentifier struct {
|
||||
ID int `db:"id"`
|
||||
Service string `db:"service"`
|
||||
SectorID string `db:"sector_id"`
|
||||
Username string `db:"username"`
|
||||
|
||||
Identifier uuid.UUID `db:"identifier"`
|
||||
}
|
|
@ -9,9 +9,9 @@ import (
|
|||
"github.com/authelia/authelia/v4/internal/model"
|
||||
)
|
||||
|
||||
// NewClient creates a new InternalClient.
|
||||
func NewClient(config schema.OpenIDConnectClientConfiguration) (client *InternalClient) {
|
||||
client = &InternalClient{
|
||||
// NewClient creates a new Client.
|
||||
func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client) {
|
||||
client = &Client{
|
||||
ID: config.ID,
|
||||
Description: config.Description,
|
||||
Secret: []byte(config.Secret),
|
||||
|
@ -37,42 +37,47 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Internal
|
|||
}
|
||||
|
||||
// IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient.
|
||||
func (c InternalClient) IsAuthenticationLevelSufficient(level authentication.Level) bool {
|
||||
func (c Client) IsAuthenticationLevelSufficient(level authentication.Level) bool {
|
||||
return authorization.IsAuthLevelSufficient(level, c.Policy)
|
||||
}
|
||||
|
||||
// GetID returns the ID.
|
||||
func (c InternalClient) GetID() string {
|
||||
func (c Client) GetID() string {
|
||||
return c.ID
|
||||
}
|
||||
|
||||
// GetConsentResponseBody returns the proper consent response body for this model.OIDCWorkflowSession.
|
||||
func (c InternalClient) GetConsentResponseBody(session *model.OIDCWorkflowSession) ConsentGetResponseBody {
|
||||
// GetSectorIdentifier returns the SectorIdentifier for this client.
|
||||
func (c Client) GetSectorIdentifier() string {
|
||||
return c.SectorIdentifier
|
||||
}
|
||||
|
||||
// GetConsentResponseBody returns the proper consent response body for this session.OIDCWorkflowSession.
|
||||
func (c Client) GetConsentResponseBody(consent *model.OAuth2ConsentSession) ConsentGetResponseBody {
|
||||
body := ConsentGetResponseBody{
|
||||
ClientID: c.ID,
|
||||
ClientDescription: c.Description,
|
||||
}
|
||||
|
||||
if session != nil {
|
||||
body.Scopes = session.RequestedScopes
|
||||
body.Audience = session.RequestedAudience
|
||||
if consent != nil {
|
||||
body.Scopes = consent.RequestedScopes
|
||||
body.Audience = consent.RequestedAudience
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
// GetHashedSecret returns the Secret.
|
||||
func (c InternalClient) GetHashedSecret() []byte {
|
||||
func (c Client) GetHashedSecret() []byte {
|
||||
return c.Secret
|
||||
}
|
||||
|
||||
// GetRedirectURIs returns the RedirectURIs.
|
||||
func (c InternalClient) GetRedirectURIs() []string {
|
||||
func (c Client) GetRedirectURIs() []string {
|
||||
return c.RedirectURIs
|
||||
}
|
||||
|
||||
// GetGrantTypes returns the GrantTypes.
|
||||
func (c InternalClient) GetGrantTypes() fosite.Arguments {
|
||||
func (c Client) GetGrantTypes() fosite.Arguments {
|
||||
if len(c.GrantTypes) == 0 {
|
||||
return fosite.Arguments{"authorization_code"}
|
||||
}
|
||||
|
@ -81,7 +86,7 @@ func (c InternalClient) GetGrantTypes() fosite.Arguments {
|
|||
}
|
||||
|
||||
// GetResponseTypes returns the ResponseTypes.
|
||||
func (c InternalClient) GetResponseTypes() fosite.Arguments {
|
||||
func (c Client) GetResponseTypes() fosite.Arguments {
|
||||
if len(c.ResponseTypes) == 0 {
|
||||
return fosite.Arguments{"code"}
|
||||
}
|
||||
|
@ -90,23 +95,23 @@ func (c InternalClient) GetResponseTypes() fosite.Arguments {
|
|||
}
|
||||
|
||||
// GetScopes returns the Scopes.
|
||||
func (c InternalClient) GetScopes() fosite.Arguments {
|
||||
func (c Client) GetScopes() fosite.Arguments {
|
||||
return c.Scopes
|
||||
}
|
||||
|
||||
// IsPublic returns the value of the Public property.
|
||||
func (c InternalClient) IsPublic() bool {
|
||||
func (c Client) IsPublic() bool {
|
||||
return c.Public
|
||||
}
|
||||
|
||||
// GetAudience returns the Audience.
|
||||
func (c InternalClient) GetAudience() fosite.Arguments {
|
||||
func (c Client) GetAudience() fosite.Arguments {
|
||||
return c.Audience
|
||||
}
|
||||
|
||||
// GetResponseModes returns the valid response modes for this client.
|
||||
//
|
||||
// Implements the fosite.ResponseModeClient.
|
||||
func (c InternalClient) GetResponseModes() []fosite.ResponseModeType {
|
||||
func (c Client) GetResponseModes() []fosite.ResponseModeType {
|
||||
return c.ResponseModes
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ func TestNewClient(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestIsAuthenticationLevelSufficient(t *testing.T) {
|
||||
c := InternalClient{}
|
||||
c := Client{}
|
||||
|
||||
c.Policy = authorization.Bypass
|
||||
assert.True(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated))
|
||||
|
@ -68,7 +68,7 @@ func TestIsAuthenticationLevelSufficient(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInternalClient_GetConsentResponseBody(t *testing.T) {
|
||||
c := InternalClient{}
|
||||
c := Client{}
|
||||
|
||||
consentRequestBody := c.GetConsentResponseBody(nil)
|
||||
assert.Equal(t, "", consentRequestBody.ClientID)
|
||||
|
@ -79,7 +79,7 @@ func TestInternalClient_GetConsentResponseBody(t *testing.T) {
|
|||
c.ID = "myclient"
|
||||
c.Description = "My Client"
|
||||
|
||||
workflow := &model.OIDCWorkflowSession{
|
||||
consent := &model.OAuth2ConsentSession{
|
||||
RequestedAudience: []string{"https://example.com"},
|
||||
RequestedScopes: []string{"openid", "groups"},
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ func TestInternalClient_GetConsentResponseBody(t *testing.T) {
|
|||
expectedScopes := []string{"openid", "groups"}
|
||||
expectedAudiences := []string{"https://example.com"}
|
||||
|
||||
consentRequestBody = c.GetConsentResponseBody(workflow)
|
||||
consentRequestBody = c.GetConsentResponseBody(consent)
|
||||
assert.Equal(t, "myclient", consentRequestBody.ClientID)
|
||||
assert.Equal(t, "My Client", consentRequestBody.ClientDescription)
|
||||
assert.Equal(t, expectedScopes, consentRequestBody.Scopes)
|
||||
|
@ -95,7 +95,7 @@ func TestInternalClient_GetConsentResponseBody(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInternalClient_GetAudience(t *testing.T) {
|
||||
c := InternalClient{}
|
||||
c := Client{}
|
||||
|
||||
audience := c.GetAudience()
|
||||
assert.Len(t, audience, 0)
|
||||
|
@ -108,7 +108,7 @@ func TestInternalClient_GetAudience(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInternalClient_GetScopes(t *testing.T) {
|
||||
c := InternalClient{}
|
||||
c := Client{}
|
||||
|
||||
scopes := c.GetScopes()
|
||||
assert.Len(t, scopes, 0)
|
||||
|
@ -121,7 +121,7 @@ func TestInternalClient_GetScopes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInternalClient_GetGrantTypes(t *testing.T) {
|
||||
c := InternalClient{}
|
||||
c := Client{}
|
||||
|
||||
grantTypes := c.GetGrantTypes()
|
||||
require.Len(t, grantTypes, 1)
|
||||
|
@ -135,7 +135,7 @@ func TestInternalClient_GetGrantTypes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInternalClient_GetHashedSecret(t *testing.T) {
|
||||
c := InternalClient{}
|
||||
c := Client{}
|
||||
|
||||
hashedSecret := c.GetHashedSecret()
|
||||
assert.Equal(t, []byte(nil), hashedSecret)
|
||||
|
@ -147,7 +147,7 @@ func TestInternalClient_GetHashedSecret(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInternalClient_GetID(t *testing.T) {
|
||||
c := InternalClient{}
|
||||
c := Client{}
|
||||
|
||||
id := c.GetID()
|
||||
assert.Equal(t, "", id)
|
||||
|
@ -159,7 +159,7 @@ func TestInternalClient_GetID(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInternalClient_GetRedirectURIs(t *testing.T) {
|
||||
c := InternalClient{}
|
||||
c := Client{}
|
||||
|
||||
redirectURIs := c.GetRedirectURIs()
|
||||
require.Len(t, redirectURIs, 0)
|
||||
|
@ -172,7 +172,7 @@ func TestInternalClient_GetRedirectURIs(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInternalClient_GetResponseModes(t *testing.T) {
|
||||
c := InternalClient{}
|
||||
c := Client{}
|
||||
|
||||
responseModes := c.GetResponseModes()
|
||||
require.Len(t, responseModes, 0)
|
||||
|
@ -191,7 +191,7 @@ func TestInternalClient_GetResponseModes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInternalClient_GetResponseTypes(t *testing.T) {
|
||||
c := InternalClient{}
|
||||
c := Client{}
|
||||
|
||||
responseTypes := c.GetResponseTypes()
|
||||
require.Len(t, responseTypes, 1)
|
||||
|
@ -206,7 +206,7 @@ func TestInternalClient_GetResponseTypes(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInternalClient_IsPublic(t *testing.T) {
|
||||
c := InternalClient{}
|
||||
c := Client{}
|
||||
|
||||
assert.False(t, c.IsPublic())
|
||||
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
package oidc
|
||||
|
||||
// NewOpenIDConnectWellKnownConfiguration generates a new OpenIDConnectWellKnownConfiguration.
|
||||
func NewOpenIDConnectWellKnownConfiguration(enablePKCEPlainChallenge, pairwise bool) (config OpenIDConnectWellKnownConfiguration) {
|
||||
config = OpenIDConnectWellKnownConfiguration{
|
||||
CommonDiscoveryOptions: CommonDiscoveryOptions{
|
||||
SubjectTypesSupported: []string{
|
||||
"public",
|
||||
},
|
||||
ResponseTypesSupported: []string{
|
||||
"code",
|
||||
"token",
|
||||
"id_token",
|
||||
"code token",
|
||||
"code id_token",
|
||||
"token id_token",
|
||||
"code token id_token",
|
||||
"none",
|
||||
},
|
||||
ResponseModesSupported: []string{
|
||||
"form_post",
|
||||
"query",
|
||||
"fragment",
|
||||
},
|
||||
ScopesSupported: []string{
|
||||
ScopeOfflineAccess,
|
||||
ScopeOpenID,
|
||||
ScopeProfile,
|
||||
ScopeGroups,
|
||||
ScopeEmail,
|
||||
},
|
||||
ClaimsSupported: []string{
|
||||
"aud",
|
||||
"exp",
|
||||
"iat",
|
||||
"iss",
|
||||
"jti",
|
||||
"rat",
|
||||
"sub",
|
||||
"auth_time",
|
||||
"nonce",
|
||||
ClaimEmail,
|
||||
ClaimEmailVerified,
|
||||
ClaimEmailAlts,
|
||||
ClaimGroups,
|
||||
ClaimPreferredUsername,
|
||||
ClaimDisplayName,
|
||||
},
|
||||
},
|
||||
OAuth2DiscoveryOptions: OAuth2DiscoveryOptions{
|
||||
CodeChallengeMethodsSupported: []string{
|
||||
"S256",
|
||||
},
|
||||
},
|
||||
OpenIDConnectDiscoveryOptions: OpenIDConnectDiscoveryOptions{
|
||||
IDTokenSigningAlgValuesSupported: []string{
|
||||
"RS256",
|
||||
},
|
||||
UserinfoSigningAlgValuesSupported: []string{
|
||||
"none",
|
||||
"RS256",
|
||||
},
|
||||
RequestObjectSigningAlgValuesSupported: []string{
|
||||
"none",
|
||||
"RS256",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if pairwise {
|
||||
config.SubjectTypesSupported = append(config.SubjectTypesSupported, "pairwise")
|
||||
}
|
||||
|
||||
if enablePKCEPlainChallenge {
|
||||
config.CodeChallengeMethodsSupported = append(config.CodeChallengeMethodsSupported, "plain")
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
pkcePlainChallenge, pairwise bool
|
||||
expectCodeChallengeMethodsSupported, expectSubjectTypesSupported []string
|
||||
}{
|
||||
{
|
||||
desc: "ShouldHaveChallengeMethodsS256ANDSubjectTypesSupportedPublic",
|
||||
pkcePlainChallenge: false,
|
||||
pairwise: false,
|
||||
expectCodeChallengeMethodsSupported: []string{"S256"},
|
||||
expectSubjectTypesSupported: []string{"public"},
|
||||
},
|
||||
{
|
||||
desc: "ShouldHaveChallengeMethodsS256PlainANDSubjectTypesSupportedPublic",
|
||||
pkcePlainChallenge: true,
|
||||
pairwise: false,
|
||||
expectCodeChallengeMethodsSupported: []string{"S256", "plain"},
|
||||
expectSubjectTypesSupported: []string{"public"},
|
||||
},
|
||||
{
|
||||
desc: "ShouldHaveChallengeMethodsS256ANDSubjectTypesSupportedPublicPairwise",
|
||||
pkcePlainChallenge: false,
|
||||
pairwise: true,
|
||||
expectCodeChallengeMethodsSupported: []string{"S256"},
|
||||
expectSubjectTypesSupported: []string{"public", "pairwise"},
|
||||
},
|
||||
{
|
||||
desc: "ShouldHaveChallengeMethodsS256PlainANDSubjectTypesSupportedPublicPairwise",
|
||||
pkcePlainChallenge: true,
|
||||
pairwise: true,
|
||||
expectCodeChallengeMethodsSupported: []string{"S256", "plain"},
|
||||
expectSubjectTypesSupported: []string{"public", "pairwise"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
actual := NewOpenIDConnectWellKnownConfiguration(tc.pkcePlainChallenge, tc.pairwise)
|
||||
for _, codeChallengeMethod := range tc.expectCodeChallengeMethodsSupported {
|
||||
assert.Contains(t, actual.CodeChallengeMethodsSupported, codeChallengeMethod)
|
||||
}
|
||||
|
||||
for _, subjectType := range tc.expectSubjectTypesSupported {
|
||||
assert.Contains(t, actual.SubjectTypesSupported, subjectType)
|
||||
}
|
||||
|
||||
for _, codeChallengeMethod := range actual.CodeChallengeMethodsSupported {
|
||||
assert.Contains(t, tc.expectCodeChallengeMethodsSupported, codeChallengeMethod)
|
||||
}
|
||||
|
||||
for _, subjectType := range actual.SubjectTypesSupported {
|
||||
assert.Contains(t, tc.expectSubjectTypesSupported, subjectType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@ import (
|
|||
)
|
||||
|
||||
// Compare compares the hash with the data and returns an error if they don't match.
|
||||
func (h AutheliaHasher) Compare(_ context.Context, hash, data []byte) (err error) {
|
||||
func (h PlainTextHasher) Compare(_ context.Context, hash, data []byte) (err error) {
|
||||
if subtle.ConstantTimeCompare(hash, data) == 0 {
|
||||
return errPasswordsDoNotMatch
|
||||
}
|
||||
|
@ -15,6 +15,6 @@ func (h AutheliaHasher) Compare(_ context.Context, hash, data []byte) (err error
|
|||
}
|
||||
|
||||
// Hash creates a new hash from data.
|
||||
func (h AutheliaHasher) Hash(_ context.Context, data []byte) (hash []byte, err error) {
|
||||
func (h PlainTextHasher) Hash(_ context.Context, data []byte) (hash []byte, err error) {
|
||||
return data, nil
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func TestShouldNotRaiseErrorOnEqualPasswordsPlainText(t *testing.T) {
|
||||
hasher := AutheliaHasher{}
|
||||
hasher := PlainTextHasher{}
|
||||
|
||||
a := []byte("abc")
|
||||
b := []byte("abc")
|
||||
|
@ -21,7 +21,7 @@ func TestShouldNotRaiseErrorOnEqualPasswordsPlainText(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestShouldRaiseErrorOnNonEqualPasswordsPlainText(t *testing.T) {
|
||||
hasher := AutheliaHasher{}
|
||||
hasher := PlainTextHasher{}
|
||||
|
||||
a := []byte("abc")
|
||||
b := []byte("abcd")
|
||||
|
@ -34,7 +34,7 @@ func TestShouldRaiseErrorOnNonEqualPasswordsPlainText(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestShouldHashPassword(t *testing.T) {
|
||||
hasher := AutheliaHasher{}
|
||||
hasher := PlainTextHasher{}
|
||||
|
||||
data := []byte("abc")
|
||||
|
||||
|
|
|
@ -8,34 +8,35 @@ import (
|
|||
"github.com/ory/herodot"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/storage"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// NewOpenIDConnectProvider new-ups a OpenIDConnectProvider.
|
||||
func NewOpenIDConnectProvider(configuration *schema.OpenIDConnectConfiguration) (provider OpenIDConnectProvider, err error) {
|
||||
func NewOpenIDConnectProvider(config *schema.OpenIDConnectConfiguration, storageProvider storage.Provider) (provider OpenIDConnectProvider, err error) {
|
||||
provider = OpenIDConnectProvider{
|
||||
Fosite: nil,
|
||||
}
|
||||
|
||||
if configuration == nil {
|
||||
if config == nil {
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
provider.Store = NewOpenIDConnectStore(configuration)
|
||||
provider.Store = NewOpenIDConnectStore(config, storageProvider)
|
||||
|
||||
composeConfiguration := &compose.Config{
|
||||
AccessTokenLifespan: configuration.AccessTokenLifespan,
|
||||
AuthorizeCodeLifespan: configuration.AuthorizeCodeLifespan,
|
||||
IDTokenLifespan: configuration.IDTokenLifespan,
|
||||
RefreshTokenLifespan: configuration.RefreshTokenLifespan,
|
||||
SendDebugMessagesToClients: configuration.EnableClientDebugMessages,
|
||||
MinParameterEntropy: configuration.MinimumParameterEntropy,
|
||||
EnforcePKCE: configuration.EnforcePKCE == "always",
|
||||
EnforcePKCEForPublicClients: configuration.EnforcePKCE != "never",
|
||||
EnablePKCEPlainChallengeMethod: configuration.EnablePKCEPlainChallenge,
|
||||
AccessTokenLifespan: config.AccessTokenLifespan,
|
||||
AuthorizeCodeLifespan: config.AuthorizeCodeLifespan,
|
||||
IDTokenLifespan: config.IDTokenLifespan,
|
||||
RefreshTokenLifespan: config.RefreshTokenLifespan,
|
||||
SendDebugMessagesToClients: config.EnableClientDebugMessages,
|
||||
MinParameterEntropy: config.MinimumParameterEntropy,
|
||||
EnforcePKCE: config.EnforcePKCE == "always",
|
||||
EnforcePKCEForPublicClients: config.EnforcePKCE != "never",
|
||||
EnablePKCEPlainChallengeMethod: config.EnablePKCEPlainChallenge,
|
||||
}
|
||||
|
||||
keyManager, err := NewKeyManagerWithConfiguration(configuration)
|
||||
keyManager, err := NewKeyManagerWithConfiguration(config)
|
||||
if err != nil {
|
||||
return provider, err
|
||||
}
|
||||
|
@ -50,7 +51,7 @@ func NewOpenIDConnectProvider(configuration *schema.OpenIDConnectConfiguration)
|
|||
strategy := &compose.CommonStrategy{
|
||||
CoreStrategy: compose.NewOAuth2HMACStrategy(
|
||||
composeConfiguration,
|
||||
[]byte(utils.HashSHA256FromString(configuration.HMACSecret)),
|
||||
[]byte(utils.HashSHA256FromString(config.HMACSecret)),
|
||||
nil,
|
||||
),
|
||||
OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy(
|
||||
|
@ -64,7 +65,7 @@ func NewOpenIDConnectProvider(configuration *schema.OpenIDConnectConfiguration)
|
|||
composeConfiguration,
|
||||
provider.Store,
|
||||
strategy,
|
||||
AutheliaHasher{},
|
||||
PlainTextHasher{},
|
||||
|
||||
/*
|
||||
These are the OAuth2 and OpenIDConnect factories. Order is important (the OAuth2 factories at the top must
|
||||
|
@ -75,7 +76,7 @@ func NewOpenIDConnectProvider(configuration *schema.OpenIDConnectConfiguration)
|
|||
compose.OAuth2AuthorizeImplicitFactory,
|
||||
compose.OAuth2ClientCredentialsGrantFactory,
|
||||
compose.OAuth2RefreshTokenGrantFactory,
|
||||
compose.OAuth2ResourceOwnerPasswordCredentialsFactory,
|
||||
// compose.OAuth2ResourceOwnerPasswordCredentialsFactory,
|
||||
// compose.RFC7523AssertionGrantFactory,.
|
||||
|
||||
compose.OpenIDConnectExplicitFactory,
|
||||
|
@ -89,80 +90,24 @@ func NewOpenIDConnectProvider(configuration *schema.OpenIDConnectConfiguration)
|
|||
compose.OAuth2PKCEFactory,
|
||||
)
|
||||
|
||||
provider.discovery = OpenIDConnectWellKnownConfiguration{
|
||||
CommonDiscoveryOptions: CommonDiscoveryOptions{
|
||||
SubjectTypesSupported: []string{
|
||||
"public",
|
||||
},
|
||||
ResponseTypesSupported: []string{
|
||||
"code",
|
||||
"token",
|
||||
"id_token",
|
||||
"code token",
|
||||
"code id_token",
|
||||
"token id_token",
|
||||
"code token id_token",
|
||||
"none",
|
||||
},
|
||||
ResponseModesSupported: []string{
|
||||
"form_post",
|
||||
"query",
|
||||
"fragment",
|
||||
},
|
||||
ScopesSupported: []string{
|
||||
ScopeOfflineAccess,
|
||||
ScopeOpenID,
|
||||
ScopeProfile,
|
||||
ScopeGroups,
|
||||
ScopeEmail,
|
||||
},
|
||||
ClaimsSupported: []string{
|
||||
"aud",
|
||||
"exp",
|
||||
"iat",
|
||||
"iss",
|
||||
"jti",
|
||||
"rat",
|
||||
"sub",
|
||||
"auth_time",
|
||||
"nonce",
|
||||
ClaimEmail,
|
||||
ClaimEmailVerified,
|
||||
ClaimEmailAlts,
|
||||
ClaimGroups,
|
||||
ClaimPreferredUsername,
|
||||
ClaimDisplayName,
|
||||
},
|
||||
},
|
||||
OAuth2DiscoveryOptions: OAuth2DiscoveryOptions{
|
||||
CodeChallengeMethodsSupported: []string{
|
||||
"S256",
|
||||
},
|
||||
},
|
||||
OpenIDConnectDiscoveryOptions: OpenIDConnectDiscoveryOptions{
|
||||
IDTokenSigningAlgValuesSupported: []string{
|
||||
"RS256",
|
||||
},
|
||||
UserinfoSigningAlgValuesSupported: []string{
|
||||
"none",
|
||||
"RS256",
|
||||
},
|
||||
RequestObjectSigningAlgValuesSupported: []string{
|
||||
"none",
|
||||
"RS256",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if configuration.EnablePKCEPlainChallenge {
|
||||
provider.discovery.CodeChallengeMethodsSupported = append(provider.discovery.CodeChallengeMethodsSupported, "plain")
|
||||
}
|
||||
provider.discovery = NewOpenIDConnectWellKnownConfiguration(config.EnablePKCEPlainChallenge, provider.Pairwise())
|
||||
|
||||
provider.herodot = herodot.NewJSONWriter(nil)
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// Pairwise returns true if this provider is configured with clients that require pairwise.
|
||||
func (p OpenIDConnectProvider) Pairwise() bool {
|
||||
for _, c := range p.Store.clients {
|
||||
if c.SectorIdentifier != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Write writes data with herodot.JSONWriter.
|
||||
func (p OpenIDConnectProvider) Write(w http.ResponseWriter, r *http.Request, e interface{}, opts ...herodot.EncoderOptions) {
|
||||
p.herodot.Write(w, r, e, opts...)
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
var exampleIssuerPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAvcMVMB2vEbqI6PlSNJ4HmUyMxBDJ5iY7FS+zDDAHOZBg9S3S\nKcAn1CZcnyL0VvJ7wcdhR6oTnOwR94eKvzUyJZ+GL2hTMm27dubEYsNdhoCl6N3X\nyEEohNfoxiiCYraVauX8X3M9jFzbEz9+pacaDbHB2syaJ1qFmMNR+HSu2jPzOo7M\nlqKIOgUzA0741MaYNt47AEVg4XU5ORLdolbAkItmYg1QbyFndg9H5IvwKkYaXTGE\nlgDBcPUC0yVjAC15Mguquq+jZeQay+6PSbHTD8PQMOkLjyChI2xEhVNbdCXe676R\ncMW2R/gjrcK23zmtmTWRfdC1iZLSlHO+bJj9vQIDAQABAoIBAEZvkP/JJOCJwqPn\nV3IcbmmilmV4bdi1vByDFgyiDyx4wOSA24+PubjvfFW9XcCgRPuKjDtTj/AhWBHv\nB7stfa2lZuNV7/u562mZArA+IAr62Zp0LdIxDV8x3T8gbjVB3HhPYbv0RJZDKTYd\nzV6jhfIrVu9mHpoY6ZnodhapCPYIyk/d49KBIHZuAc25CUjMXgTeaVtf0c996036\nUxW6ef33wAOJAvW0RCvbXAJfmBeEq2qQlkjTIlpYx71fhZWexHifi8Ouv3Zonc+1\n/P2Adq5uzYVBT92f9RKHg9QxxNzVrLjSMaxyvUtWQCAQfW0tFIRdqBGsHYsQrFtI\nF4yzv8ECgYEA7ntpyN9HD9Z9lYQzPCR73sFCLM+ID99aVij0wHuxK97bkSyyvkLd\n7MyTaym3lg1UEqWNWBCLvFULZx7F0Ah6qCzD4ymm3Bj/ADpWWPgljBI0AFml+HHs\nhcATmXUrj5QbLyhiP2gmJjajp1o/rgATx6ED66seSynD6JOH8wUhhZUCgYEAy7OA\n06PF8GfseNsTqlDjNF0K7lOqd21S0prdwrsJLiVzUlfMM25MLE0XLDUutCnRheeh\nIlcuDoBsVTxz6rkvFGD74N+pgXlN4CicsBq5ofK060PbqCQhSII3fmHobrZ9Cr75\nHmBjAxHx998SKaAAGbBbcYGUAp521i1pH5CEPYkCgYEAkUd1Zf0+2RMdZhwm6hh/\nrW+l1I6IoMK70YkZsLipccRNld7Y9LbfYwYtODcts6di9AkOVfueZJiaXbONZfIE\nZrb+jkAteh9wGL9xIrnohbABJcV3Kiaco84jInUSmGDtPokncOENfHIEuEpuSJ2b\nbx1TuhmAVuGWivR0+ULC7RECgYEAgS0cDRpWc9Xzh9Cl7+PLsXEvdWNpPsL9OsEq\n0Ep7z9+/+f/jZtoTRCS/BTHUpDvAuwHglT5j3p5iFMt5VuiIiovWLwynGYwrbnNS\nqfrIrYKUaH1n1oDS+oBZYLQGCe9/7EifAjxtjYzbvSyg//SPG7tSwfBCREbpZXj2\nqSWkNsECgYA/mCDzCTlrrWPuiepo6kTmN+4TnFA+hJI6NccDVQ+jvbqEdoJ4SW4L\nzqfZSZRFJMNpSgIqkQNRPJqMP0jQ5KRtJrjMWBnYxktwKz9fDg2R2MxdFgMF2LH2\nHEMMhFHlv8NDjVOXh1KwRoltNGVWYsSrD9wKU9GhRCEfmNCGrvBcEg==\n-----END RSA PRIVATE KEY-----"
|
||||
|
||||
func TestOpenIDConnectProvider_NewOpenIDConnectProvider_NotConfigured(t *testing.T) {
|
||||
provider, err := NewOpenIDConnectProvider(nil)
|
||||
provider, err := NewOpenIDConnectProvider(nil, nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, provider.Fosite)
|
||||
|
@ -22,7 +22,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_NotConfigured(t *testing
|
|||
func TestOpenIDConnectProvider_NewOpenIDConnectProvider_BadIssuerKey(t *testing.T) {
|
||||
_, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{
|
||||
IssuerPrivateKey: "BAD KEY",
|
||||
})
|
||||
}, nil)
|
||||
|
||||
assert.Error(t, err, "abc")
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *tes
|
|||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}, nil)
|
||||
|
||||
assert.NotNil(t, provider)
|
||||
assert.NoError(t, err)
|
||||
|
@ -80,10 +80,12 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow
|
|||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}, nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.False(t, provider.Pairwise())
|
||||
|
||||
disco := provider.GetOpenIDConnectWellKnownConfiguration("https://example.com")
|
||||
|
||||
assert.Equal(t, "https://example.com", disco.Issuer)
|
||||
|
@ -95,8 +97,8 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow
|
|||
assert.Equal(t, "https://example.com/api/oidc/revocation", disco.RevocationEndpoint)
|
||||
assert.Equal(t, "", disco.RegistrationEndpoint)
|
||||
|
||||
require.Len(t, disco.CodeChallengeMethodsSupported, 1)
|
||||
assert.Equal(t, "S256", disco.CodeChallengeMethodsSupported[0])
|
||||
assert.Len(t, disco.CodeChallengeMethodsSupported, 1)
|
||||
assert.Contains(t, disco.CodeChallengeMethodsSupported, "S256")
|
||||
|
||||
assert.Len(t, disco.ScopesSupported, 5)
|
||||
assert.Contains(t, disco.ScopesSupported, ScopeOpenID)
|
||||
|
@ -166,7 +168,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfig
|
|||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}, nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -241,7 +243,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow
|
|||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}, nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
|
@ -2,38 +2,32 @@ package oidc
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/storage"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authorization"
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
"github.com/authelia/authelia/v4/internal/logging"
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
"github.com/authelia/authelia/v4/internal/storage"
|
||||
)
|
||||
|
||||
// NewOpenIDConnectStore returns a new OpenIDConnectStore using the provided schema.OpenIDConnectConfiguration.
|
||||
func NewOpenIDConnectStore(configuration *schema.OpenIDConnectConfiguration) (store *OpenIDConnectStore) {
|
||||
// NewOpenIDConnectStore returns a OpenIDConnectStore when provided with a schema.OpenIDConnectConfiguration and storage.Provider.
|
||||
func NewOpenIDConnectStore(config *schema.OpenIDConnectConfiguration, provider storage.Provider) (store *OpenIDConnectStore) {
|
||||
logger := logging.Logger()
|
||||
|
||||
store = &OpenIDConnectStore{
|
||||
memory: &storage.MemoryStore{
|
||||
IDSessions: map[string]fosite.Requester{},
|
||||
Users: map[string]storage.MemoryUserRelation{},
|
||||
AuthorizeCodes: map[string]storage.StoreAuthorizeCode{},
|
||||
AccessTokens: map[string]fosite.Requester{},
|
||||
RefreshTokens: map[string]storage.StoreRefreshToken{},
|
||||
PKCES: map[string]fosite.Requester{},
|
||||
BlacklistedJTIs: map[string]time.Time{},
|
||||
AccessTokenRequestIDs: map[string]string{},
|
||||
RefreshTokenRequestIDs: map[string]string{},
|
||||
},
|
||||
provider: provider,
|
||||
clients: map[string]*Client{},
|
||||
}
|
||||
|
||||
store.clients = make(map[string]*InternalClient)
|
||||
|
||||
for _, client := range configuration.Clients {
|
||||
for _, client := range config.Clients {
|
||||
policy := authorization.PolicyToLevel(client.Policy)
|
||||
logger.Debugf("Registering client %s with policy %s (%v)", client.ID, client.Policy, policy)
|
||||
|
||||
|
@ -43,9 +37,37 @@ func NewOpenIDConnectStore(configuration *schema.OpenIDConnectConfiguration) (st
|
|||
return store
|
||||
}
|
||||
|
||||
// GenerateOpaqueUserID either retrieves or creates an opaque user id from a sectorID and username.
|
||||
func (s OpenIDConnectStore) GenerateOpaqueUserID(ctx context.Context, sectorID, username string) (opaqueID *model.UserOpaqueIdentifier, err error) {
|
||||
if opaqueID, err = s.provider.LoadUserOpaqueIdentifierBySignature(ctx, "openid", sectorID, username); err != nil {
|
||||
return nil, err
|
||||
} else if opaqueID == nil {
|
||||
if opaqueID, err = model.NewUserOpaqueIdentifier("openid", sectorID, username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = s.provider.SaveUserOpaqueIdentifier(ctx, *opaqueID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return opaqueID, nil
|
||||
}
|
||||
|
||||
// GetSubject returns a subject UUID for a username. If it exists, it returns the existing one, otherwise it creates and saves it.
|
||||
func (s OpenIDConnectStore) GetSubject(ctx context.Context, sectorID, username string) (subject uuid.UUID, err error) {
|
||||
var opaqueID *model.UserOpaqueIdentifier
|
||||
|
||||
if opaqueID, err = s.GenerateOpaqueUserID(ctx, sectorID, username); err != nil {
|
||||
return uuid.UUID{}, err
|
||||
}
|
||||
|
||||
return opaqueID.Identifier, nil
|
||||
}
|
||||
|
||||
// GetClientPolicy retrieves the policy from the client with the matching provided id.
|
||||
func (s OpenIDConnectStore) GetClientPolicy(id string) (level authorization.Level) {
|
||||
client, err := s.GetInternalClient(id)
|
||||
client, err := s.GetFullClient(id)
|
||||
if err != nil {
|
||||
return authorization.TwoFactor
|
||||
}
|
||||
|
@ -53,8 +75,8 @@ func (s OpenIDConnectStore) GetClientPolicy(id string) (level authorization.Leve
|
|||
return client.Policy
|
||||
}
|
||||
|
||||
// GetInternalClient returns a fosite.Client asserted as an InternalClient matching the provided id.
|
||||
func (s OpenIDConnectStore) GetInternalClient(id string) (client *InternalClient, err error) {
|
||||
// GetFullClient returns a fosite.Client asserted as an Client matching the provided id.
|
||||
func (s OpenIDConnectStore) GetFullClient(id string) (client *Client, err error) {
|
||||
client, ok := s.clients[id]
|
||||
if !ok {
|
||||
return nil, fosite.ErrNotFound
|
||||
|
@ -65,142 +87,248 @@ func (s OpenIDConnectStore) GetInternalClient(id string) (client *InternalClient
|
|||
|
||||
// IsValidClientID returns true if the provided id exists in the OpenIDConnectProvider.Clients map.
|
||||
func (s OpenIDConnectStore) IsValidClientID(id string) (valid bool) {
|
||||
_, err := s.GetInternalClient(id)
|
||||
_, err := s.GetFullClient(id)
|
||||
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// CreateOpenIDConnectSession decorates fosite's storage.MemoryStore CreateOpenIDConnectSession method.
|
||||
func (s *OpenIDConnectStore) CreateOpenIDConnectSession(ctx context.Context, authorizeCode string, requester fosite.Requester) error {
|
||||
return s.memory.CreateOpenIDConnectSession(ctx, authorizeCode, requester)
|
||||
// BeginTX starts a transaction.
|
||||
// This implements a portion of fosite storage.Transactional interface.
|
||||
func (s *OpenIDConnectStore) BeginTX(ctx context.Context) (c context.Context, err error) {
|
||||
return s.provider.BeginTX(ctx)
|
||||
}
|
||||
|
||||
// GetOpenIDConnectSession decorates fosite's storage.MemoryStore GetOpenIDConnectSession method.
|
||||
func (s *OpenIDConnectStore) GetOpenIDConnectSession(ctx context.Context, authorizeCode string, requester fosite.Requester) (fosite.Requester, error) {
|
||||
return s.memory.GetOpenIDConnectSession(ctx, authorizeCode, requester)
|
||||
// Commit completes a transaction.
|
||||
// This implements a portion of fosite storage.Transactional interface.
|
||||
func (s *OpenIDConnectStore) Commit(ctx context.Context) (err error) {
|
||||
return s.provider.Commit(ctx)
|
||||
}
|
||||
|
||||
// DeleteOpenIDConnectSession decorates fosite's storage.MemoryStore DeleteOpenIDConnectSession method.
|
||||
func (s *OpenIDConnectStore) DeleteOpenIDConnectSession(ctx context.Context, authorizeCode string) error {
|
||||
return s.memory.DeleteOpenIDConnectSession(ctx, authorizeCode)
|
||||
// Rollback rolls a transaction back.
|
||||
// This implements a portion of fosite storage.Transactional interface.
|
||||
func (s *OpenIDConnectStore) Rollback(ctx context.Context) (err error) {
|
||||
return s.provider.Rollback(ctx)
|
||||
}
|
||||
|
||||
// GetClient decorates fosite's storage.MemoryStore GetClient method.
|
||||
func (s *OpenIDConnectStore) GetClient(_ context.Context, id string) (fosite.Client, error) {
|
||||
return s.GetInternalClient(id)
|
||||
// GetClient loads the client by its ID or returns an error if the client does not exist or another error occurred.
|
||||
// This implements a portion of fosite.ClientManager.
|
||||
func (s *OpenIDConnectStore) GetClient(_ context.Context, id string) (client fosite.Client, err error) {
|
||||
return s.GetFullClient(id)
|
||||
}
|
||||
|
||||
// ClientAssertionJWTValid decorates fosite's storage.MemoryStore ClientAssertionJWTValid method.
|
||||
func (s *OpenIDConnectStore) ClientAssertionJWTValid(ctx context.Context, jti string) error {
|
||||
return s.memory.ClientAssertionJWTValid(ctx, jti)
|
||||
// ClientAssertionJWTValid returns an error if the JTI is known or the DB check failed and nil if the JTI is not known.
|
||||
// This implements a portion of fosite.ClientManager.
|
||||
func (s *OpenIDConnectStore) ClientAssertionJWTValid(ctx context.Context, jti string) (err error) {
|
||||
signature := fmt.Sprintf("%x", sha256.Sum256([]byte(jti)))
|
||||
|
||||
blacklistedJTI, err := s.provider.LoadOAuth2BlacklistedJTI(ctx, signature)
|
||||
|
||||
switch {
|
||||
case errors.Is(sql.ErrNoRows, err):
|
||||
return nil
|
||||
case err != nil:
|
||||
return err
|
||||
case blacklistedJTI.ExpiresAt.After(time.Now()):
|
||||
return fosite.ErrJTIKnown
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// SetClientAssertionJWT decorates fosite's storage.MemoryStore SetClientAssertionJWT method.
|
||||
func (s *OpenIDConnectStore) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) error {
|
||||
return s.memory.SetClientAssertionJWT(ctx, jti, exp)
|
||||
// SetClientAssertionJWT marks a JTI as known for the given expiry time. Before inserting the new JTI, it will clean
|
||||
// up any existing JTIs that have expired as those tokens can not be replayed due to the expiry.
|
||||
// This implements a portion of fosite.ClientManager.
|
||||
func (s *OpenIDConnectStore) SetClientAssertionJWT(ctx context.Context, jti string, exp time.Time) (err error) {
|
||||
blacklistedJTI := model.NewOAuth2BlacklistedJTI(jti, exp)
|
||||
|
||||
return s.provider.SaveOAuth2BlacklistedJTI(ctx, blacklistedJTI)
|
||||
}
|
||||
|
||||
// CreateAuthorizeCodeSession decorates fosite's storage.MemoryStore CreateAuthorizeCodeSession method.
|
||||
func (s *OpenIDConnectStore) CreateAuthorizeCodeSession(ctx context.Context, code string, req fosite.Requester) error {
|
||||
return s.memory.CreateAuthorizeCodeSession(ctx, code, req)
|
||||
// CreateAuthorizeCodeSession stores the authorization request for a given authorization code.
|
||||
// This implements a portion of oauth2.AuthorizeCodeStorage.
|
||||
func (s *OpenIDConnectStore) CreateAuthorizeCodeSession(ctx context.Context, code string, request fosite.Requester) (err error) {
|
||||
return s.saveSession(ctx, storage.OAuth2SessionTypeAuthorizeCode, code, request)
|
||||
}
|
||||
|
||||
// GetAuthorizeCodeSession decorates fosite's storage.MemoryStore GetAuthorizeCodeSession method.
|
||||
func (s *OpenIDConnectStore) GetAuthorizeCodeSession(ctx context.Context, code string, session fosite.Session) (fosite.Requester, error) {
|
||||
return s.memory.GetAuthorizeCodeSession(ctx, code, session)
|
||||
// InvalidateAuthorizeCodeSession is called when an authorize code is being used. The state of the authorization
|
||||
// code should be set to invalid and consecutive requests to GetAuthorizeCodeSession should return the
|
||||
// ErrInvalidatedAuthorizeCode error.
|
||||
// This implements a portion of oauth2.AuthorizeCodeStorage.
|
||||
func (s *OpenIDConnectStore) InvalidateAuthorizeCodeSession(ctx context.Context, code string) (err error) {
|
||||
return s.provider.DeactivateOAuth2Session(ctx, storage.OAuth2SessionTypeAuthorizeCode, code)
|
||||
}
|
||||
|
||||
// InvalidateAuthorizeCodeSession decorates fosite's storage.MemoryStore InvalidateAuthorizeCodeSession method.
|
||||
func (s *OpenIDConnectStore) InvalidateAuthorizeCodeSession(ctx context.Context, code string) error {
|
||||
return s.memory.InvalidateAuthorizeCodeSession(ctx, code)
|
||||
// GetAuthorizeCodeSession hydrates the session based on the given code and returns the authorization request.
|
||||
// If the authorization code has been invalidated with `InvalidateAuthorizeCodeSession`, this
|
||||
// method should return the ErrInvalidatedAuthorizeCode error.
|
||||
// Make sure to also return the fosite.Requester value when returning the fosite.ErrInvalidatedAuthorizeCode error!
|
||||
// This implements a portion of oauth2.AuthorizeCodeStorage.
|
||||
func (s *OpenIDConnectStore) GetAuthorizeCodeSession(ctx context.Context, code string, session fosite.Session) (request fosite.Requester, err error) {
|
||||
// TODO: Implement the fosite.ErrInvalidatedAuthorizeCode error above. This requires splitting the invalidated sessions and deleted sessions.
|
||||
return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypeAuthorizeCode, code, session)
|
||||
}
|
||||
|
||||
// CreatePKCERequestSession decorates fosite's storage.MemoryStore CreatePKCERequestSession method.
|
||||
func (s *OpenIDConnectStore) CreatePKCERequestSession(ctx context.Context, code string, req fosite.Requester) error {
|
||||
return s.memory.CreatePKCERequestSession(ctx, code, req)
|
||||
// CreateAccessTokenSession stores the authorization request for a given access token.
|
||||
// This implements a portion of oauth2.AccessTokenStorage.
|
||||
func (s *OpenIDConnectStore) CreateAccessTokenSession(ctx context.Context, signature string, request fosite.Requester) (err error) {
|
||||
return s.saveSession(ctx, storage.OAuth2SessionTypeAccessToken, signature, request)
|
||||
}
|
||||
|
||||
// GetPKCERequestSession decorates fosite's storage.MemoryStore GetPKCERequestSession method.
|
||||
func (s *OpenIDConnectStore) GetPKCERequestSession(ctx context.Context, code string, session fosite.Session) (fosite.Requester, error) {
|
||||
return s.memory.GetPKCERequestSession(ctx, code, session)
|
||||
// DeleteAccessTokenSession marks an access token session as deleted.
|
||||
// This implements a portion of oauth2.AccessTokenStorage.
|
||||
func (s *OpenIDConnectStore) DeleteAccessTokenSession(ctx context.Context, signature string) (err error) {
|
||||
return s.revokeSessionBySignature(ctx, storage.OAuth2SessionTypeAccessToken, signature)
|
||||
}
|
||||
|
||||
// DeletePKCERequestSession decorates fosite's storage.MemoryStore DeletePKCERequestSession method.
|
||||
func (s *OpenIDConnectStore) DeletePKCERequestSession(ctx context.Context, code string) error {
|
||||
return s.memory.DeletePKCERequestSession(ctx, code)
|
||||
// RevokeAccessToken revokes an access token as specified in: https://tools.ietf.org/html/rfc7009#section-2.1
|
||||
// If the token passed to the request is an access token, the server MAY revoke the respective refresh token as well.
|
||||
// This implements a portion of oauth2.TokenRevocationStorage.
|
||||
func (s *OpenIDConnectStore) RevokeAccessToken(ctx context.Context, requestID string) (err error) {
|
||||
return s.revokeSessionByRequestID(ctx, storage.OAuth2SessionTypeAccessToken, requestID)
|
||||
}
|
||||
|
||||
// CreateAccessTokenSession decorates fosite's storage.MemoryStore CreateAccessTokenSession method.
|
||||
func (s *OpenIDConnectStore) CreateAccessTokenSession(ctx context.Context, signature string, req fosite.Requester) error {
|
||||
return s.memory.CreateAccessTokenSession(ctx, signature, req)
|
||||
// GetAccessTokenSession gets the authorization request for a given access token.
|
||||
// This implements a portion of oauth2.AccessTokenStorage.
|
||||
func (s *OpenIDConnectStore) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) {
|
||||
return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypeAccessToken, signature, session)
|
||||
}
|
||||
|
||||
// GetAccessTokenSession decorates fosite's storage.MemoryStore GetAccessTokenSession method.
|
||||
func (s *OpenIDConnectStore) GetAccessTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error) {
|
||||
return s.memory.GetAccessTokenSession(ctx, signature, session)
|
||||
// CreateRefreshTokenSession stores the authorization request for a given refresh token.
|
||||
// This implements a portion of oauth2.RefreshTokenStorage.
|
||||
func (s *OpenIDConnectStore) CreateRefreshTokenSession(ctx context.Context, signature string, request fosite.Requester) (err error) {
|
||||
return s.saveSession(ctx, storage.OAuth2SessionTypeRefreshToken, signature, request)
|
||||
}
|
||||
|
||||
// DeleteAccessTokenSession decorates fosite's storage.MemoryStore DeleteAccessTokenSession method.
|
||||
func (s *OpenIDConnectStore) DeleteAccessTokenSession(ctx context.Context, signature string) error {
|
||||
return s.memory.DeleteAccessTokenSession(ctx, signature)
|
||||
// DeleteRefreshTokenSession marks the authorization request for a given refresh token as deleted.
|
||||
// This implements a portion of oauth2.RefreshTokenStorage.
|
||||
func (s *OpenIDConnectStore) DeleteRefreshTokenSession(ctx context.Context, signature string) (err error) {
|
||||
return s.revokeSessionBySignature(ctx, storage.OAuth2SessionTypeRefreshToken, signature)
|
||||
}
|
||||
|
||||
// CreateRefreshTokenSession decorates fosite's storage.MemoryStore CreateRefreshTokenSession method.
|
||||
func (s *OpenIDConnectStore) CreateRefreshTokenSession(ctx context.Context, signature string, req fosite.Requester) error {
|
||||
return s.memory.CreateRefreshTokenSession(ctx, signature, req)
|
||||
// RevokeRefreshToken revokes a refresh token as specified in: https://tools.ietf.org/html/rfc7009#section-2.1
|
||||
// If the particular token is a refresh token and the authorization server supports the revocation of access tokens,
|
||||
// then the authorization server SHOULD also invalidate all access tokens based on the same authorization grant (see Implementation Note).
|
||||
// This implements a portion of oauth2.TokenRevocationStorage.
|
||||
func (s *OpenIDConnectStore) RevokeRefreshToken(ctx context.Context, requestID string) (err error) {
|
||||
return s.provider.DeactivateOAuth2SessionByRequestID(ctx, storage.OAuth2SessionTypeRefreshToken, requestID)
|
||||
}
|
||||
|
||||
// GetRefreshTokenSession decorates fosite's storage.MemoryStore GetRefreshTokenSession method.
|
||||
func (s *OpenIDConnectStore) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (fosite.Requester, error) {
|
||||
return s.memory.GetRefreshTokenSession(ctx, signature, session)
|
||||
// RevokeRefreshTokenMaybeGracePeriod revokes an access token as specified in: https://tools.ietf.org/html/rfc7009#section-2.1
|
||||
// If the token passed to the request is an access token, the server MAY revoke the respective refresh token as well.
|
||||
// This implements a portion of oauth2.TokenRevocationStorage.
|
||||
func (s *OpenIDConnectStore) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, signature string) (err error) {
|
||||
return s.RevokeRefreshToken(ctx, requestID)
|
||||
}
|
||||
|
||||
// DeleteRefreshTokenSession decorates fosite's storage.MemoryStore DeleteRefreshTokenSession method.
|
||||
func (s *OpenIDConnectStore) DeleteRefreshTokenSession(ctx context.Context, signature string) error {
|
||||
return s.memory.DeleteRefreshTokenSession(ctx, signature)
|
||||
// GetRefreshTokenSession gets the authorization request for a given refresh token.
|
||||
// This implements a portion of oauth2.RefreshTokenStorage.
|
||||
func (s *OpenIDConnectStore) GetRefreshTokenSession(ctx context.Context, signature string, session fosite.Session) (request fosite.Requester, err error) {
|
||||
return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypeRefreshToken, signature, session)
|
||||
}
|
||||
|
||||
// Authenticate decorates fosite's storage.MemoryStore Authenticate method.
|
||||
func (s *OpenIDConnectStore) Authenticate(ctx context.Context, name string, secret string) error {
|
||||
return s.memory.Authenticate(ctx, name, secret)
|
||||
// CreatePKCERequestSession stores the authorization request for a given PKCE request.
|
||||
// This implements a portion of pkce.PKCERequestStorage.
|
||||
func (s *OpenIDConnectStore) CreatePKCERequestSession(ctx context.Context, signature string, request fosite.Requester) (err error) {
|
||||
return s.saveSession(ctx, storage.OAuth2SessionTypePKCEChallenge, signature, request)
|
||||
}
|
||||
|
||||
// RevokeRefreshToken decorates fosite's storage.MemoryStore RevokeRefreshToken method.
|
||||
func (s *OpenIDConnectStore) RevokeRefreshToken(ctx context.Context, requestID string) error {
|
||||
return s.memory.RevokeRefreshToken(ctx, requestID)
|
||||
// DeletePKCERequestSession marks the authorization request for a given PKCE request as deleted.
|
||||
// This implements a portion of pkce.PKCERequestStorage.
|
||||
func (s *OpenIDConnectStore) DeletePKCERequestSession(ctx context.Context, signature string) (err error) {
|
||||
return s.revokeSessionBySignature(ctx, storage.OAuth2SessionTypeAccessToken, signature)
|
||||
}
|
||||
|
||||
// RevokeRefreshTokenMaybeGracePeriod decorates fosite's storage.MemoryStore RevokeRefreshTokenMaybeGracePeriod method.
|
||||
func (s OpenIDConnectStore) RevokeRefreshTokenMaybeGracePeriod(ctx context.Context, requestID string, signature string) error {
|
||||
return s.memory.RevokeRefreshTokenMaybeGracePeriod(ctx, requestID, signature)
|
||||
// GetPKCERequestSession gets the authorization request for a given PKCE request.
|
||||
// This implements a portion of pkce.PKCERequestStorage.
|
||||
func (s *OpenIDConnectStore) GetPKCERequestSession(ctx context.Context, signature string, session fosite.Session) (requester fosite.Requester, err error) {
|
||||
return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypePKCEChallenge, signature, session)
|
||||
}
|
||||
|
||||
// RevokeAccessToken decorates fosite's storage.MemoryStore RevokeAccessToken method.
|
||||
func (s *OpenIDConnectStore) RevokeAccessToken(ctx context.Context, requestID string) error {
|
||||
return s.memory.RevokeAccessToken(ctx, requestID)
|
||||
// CreateOpenIDConnectSession creates an open id connect session for a given authorize code.
|
||||
// This is relevant for explicit open id connect flow.
|
||||
// This implements a portion of openid.OpenIDConnectRequestStorage.
|
||||
func (s *OpenIDConnectStore) CreateOpenIDConnectSession(ctx context.Context, authorizeCode string, request fosite.Requester) (err error) {
|
||||
return s.saveSession(ctx, storage.OAuth2SessionTypeOpenIDConnect, authorizeCode, request)
|
||||
}
|
||||
|
||||
// GetPublicKey decorates fosite's storage.MemoryStore GetPublicKey method.
|
||||
func (s *OpenIDConnectStore) GetPublicKey(ctx context.Context, issuer string, subject string, keyID string) (*jose.JSONWebKey, error) {
|
||||
return s.memory.GetPublicKey(ctx, issuer, subject, keyID)
|
||||
// DeleteOpenIDConnectSession just implements the method required by fosite even though it's unused.
|
||||
// This implements a portion of openid.OpenIDConnectRequestStorage.
|
||||
func (s *OpenIDConnectStore) DeleteOpenIDConnectSession(ctx context.Context, authorizeCode string) (err error) {
|
||||
return s.revokeSessionBySignature(ctx, storage.OAuth2SessionTypeAccessToken, authorizeCode)
|
||||
}
|
||||
|
||||
// GetPublicKeys decorates fosite's storage.MemoryStore GetPublicKeys method.
|
||||
func (s *OpenIDConnectStore) GetPublicKeys(ctx context.Context, issuer string, subject string) (*jose.JSONWebKeySet, error) {
|
||||
return s.memory.GetPublicKeys(ctx, issuer, subject)
|
||||
// GetOpenIDConnectSession returns error:
|
||||
// - nil if a session was found,
|
||||
// - ErrNoSessionFound if no session was found
|
||||
// - or an arbitrary error if an error occurred.
|
||||
// This implements a portion of openid.OpenIDConnectRequestStorage.
|
||||
func (s *OpenIDConnectStore) GetOpenIDConnectSession(ctx context.Context, authorizeCode string, request fosite.Requester) (r fosite.Requester, err error) {
|
||||
return s.loadSessionBySignature(ctx, storage.OAuth2SessionTypeOpenIDConnect, authorizeCode, request.GetSession())
|
||||
}
|
||||
|
||||
// GetPublicKeyScopes decorates fosite's storage.MemoryStore GetPublicKeyScopes method.
|
||||
func (s *OpenIDConnectStore) GetPublicKeyScopes(ctx context.Context, issuer string, subject string, keyID string) ([]string, error) {
|
||||
return s.memory.GetPublicKeyScopes(ctx, issuer, subject, keyID)
|
||||
// IsJWTUsed implements an interface required for RFC7523.
|
||||
func (s *OpenIDConnectStore) IsJWTUsed(ctx context.Context, jti string) (used bool, err error) {
|
||||
if err = s.ClientAssertionJWTValid(ctx, jti); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// IsJWTUsed decorates fosite's storage.MemoryStore IsJWTUsed method.
|
||||
func (s *OpenIDConnectStore) IsJWTUsed(ctx context.Context, jti string) (bool, error) {
|
||||
return s.memory.IsJWTUsed(ctx, jti)
|
||||
// MarkJWTUsedForTime implements an interface required for rfc7523.RFC7523KeyStorage.
|
||||
func (s *OpenIDConnectStore) MarkJWTUsedForTime(ctx context.Context, jti string, exp time.Time) (err error) {
|
||||
return s.SetClientAssertionJWT(ctx, jti, exp)
|
||||
}
|
||||
|
||||
// MarkJWTUsedForTime decorates fosite's storage.MemoryStore MarkJWTUsedForTime method.
|
||||
func (s *OpenIDConnectStore) MarkJWTUsedForTime(ctx context.Context, jti string, exp time.Time) error {
|
||||
return s.memory.MarkJWTUsedForTime(ctx, jti, exp)
|
||||
func (s *OpenIDConnectStore) loadSessionBySignature(ctx context.Context, sessionType storage.OAuth2SessionType, signature string, session fosite.Session) (r fosite.Requester, err error) {
|
||||
var (
|
||||
sessionModel *model.OAuth2Session
|
||||
)
|
||||
|
||||
sessionModel, err = s.provider.LoadOAuth2Session(ctx, sessionType, signature)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return nil, fosite.ErrNotFound
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if r, err = sessionModel.ToRequest(ctx, session, s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !sessionModel.Active && sessionType == storage.OAuth2SessionTypeAuthorizeCode {
|
||||
return r, fosite.ErrInvalidatedAuthorizeCode
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (s *OpenIDConnectStore) saveSession(ctx context.Context, sessionType storage.OAuth2SessionType, signature string, r fosite.Requester) (err error) {
|
||||
var session *model.OAuth2Session
|
||||
|
||||
if session, err = model.NewOAuth2SessionFromRequest(signature, r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.provider.SaveOAuth2Session(ctx, sessionType, *session)
|
||||
}
|
||||
|
||||
func (s *OpenIDConnectStore) revokeSessionBySignature(ctx context.Context, sessionType storage.OAuth2SessionType, signature string) (err error) {
|
||||
return s.provider.RevokeOAuth2Session(ctx, sessionType, signature)
|
||||
}
|
||||
|
||||
func (s *OpenIDConnectStore) revokeSessionByRequestID(ctx context.Context, sessionType storage.OAuth2SessionType, requestID string) (err error) {
|
||||
if err = s.provider.RevokeOAuth2SessionByRequestID(ctx, sessionType, requestID); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return fosite.ErrNotFound
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,15 +1,6 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authorization"
|
||||
"github.com/authelia/authelia/v4/internal/configuration/schema"
|
||||
)
|
||||
/*
|
||||
|
||||
func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) {
|
||||
s := NewOpenIDConnectStore(&schema.OpenIDConnectConfiguration{
|
||||
|
@ -80,7 +71,7 @@ func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) {
|
|||
Clients: []schema.OpenIDConnectClientConfiguration{c1},
|
||||
})
|
||||
|
||||
client, err := s.GetInternalClient(c1.ID)
|
||||
client, err := s.GetFullClient(c1.ID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, client)
|
||||
assert.Equal(t, client.ID, c1.ID)
|
||||
|
@ -107,7 +98,7 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) {
|
|||
Clients: []schema.OpenIDConnectClientConfiguration{c1},
|
||||
})
|
||||
|
||||
client, err := s.GetInternalClient("another-client")
|
||||
client, err := s.GetFullClient("another-client")
|
||||
assert.Nil(t, client)
|
||||
assert.EqualError(t, err, "not_found")
|
||||
}
|
||||
|
@ -131,4 +122,5 @@ func TestOpenIDConnectStore_IsValidClientID(t *testing.T) {
|
|||
|
||||
assert.True(t, validClient)
|
||||
assert.False(t, invalidClient)
|
||||
}
|
||||
}.
|
||||
*/
|
||||
|
|
|
@ -6,17 +6,19 @@ import (
|
|||
|
||||
"github.com/ory/fosite"
|
||||
"github.com/ory/fosite/handler/openid"
|
||||
"github.com/ory/fosite/storage"
|
||||
"github.com/ory/fosite/token/jwt"
|
||||
"github.com/ory/herodot"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authorization"
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
"github.com/authelia/authelia/v4/internal/storage"
|
||||
"github.com/authelia/authelia/v4/internal/utils"
|
||||
)
|
||||
|
||||
// NewSession creates a new OpenIDSession struct.
|
||||
func NewSession() (session *OpenIDSession) {
|
||||
return &OpenIDSession{
|
||||
// NewSession creates a new empty OpenIDSession struct.
|
||||
func NewSession() (session *model.OpenIDSession) {
|
||||
return &model.OpenIDSession{
|
||||
DefaultSession: &openid.DefaultSession{
|
||||
Claims: &jwt.IDTokenClaims{
|
||||
Extra: map[string]interface{}{},
|
||||
|
@ -30,19 +32,19 @@ func NewSession() (session *OpenIDSession) {
|
|||
}
|
||||
|
||||
// NewSessionWithAuthorizeRequest uses details from an AuthorizeRequester to generate an OpenIDSession.
|
||||
func NewSessionWithAuthorizeRequest(issuer, kid, subject, username string, amr []string, extra map[string]interface{},
|
||||
authTime, requestedAt time.Time, requester fosite.AuthorizeRequester) (session *OpenIDSession) {
|
||||
func NewSessionWithAuthorizeRequest(issuer, kid, username string, amr []string, extra map[string]interface{},
|
||||
authTime time.Time, consent *model.OAuth2ConsentSession, requester fosite.AuthorizeRequester) (session *model.OpenIDSession) {
|
||||
if extra == nil {
|
||||
extra = make(map[string]interface{})
|
||||
}
|
||||
|
||||
return &OpenIDSession{
|
||||
session = &model.OpenIDSession{
|
||||
DefaultSession: &openid.DefaultSession{
|
||||
Claims: &jwt.IDTokenClaims{
|
||||
Subject: subject,
|
||||
Subject: consent.Subject.String(),
|
||||
Issuer: issuer,
|
||||
AuthTime: authTime,
|
||||
RequestedAt: requestedAt,
|
||||
RequestedAt: consent.RequestedAt,
|
||||
IssuedAt: time.Now(),
|
||||
Nonce: requester.GetRequestForm().Get("nonce"),
|
||||
Audience: requester.GetGrantedAudience(),
|
||||
|
@ -55,12 +57,20 @@ func NewSessionWithAuthorizeRequest(issuer, kid, subject, username string, amr [
|
|||
"kid": kid,
|
||||
},
|
||||
},
|
||||
Subject: subject,
|
||||
Subject: consent.Subject.String(),
|
||||
Username: username,
|
||||
},
|
||||
Extra: map[string]interface{}{},
|
||||
ClientID: requester.GetClient().GetID(),
|
||||
ChallengeID: consent.ChallengeID,
|
||||
}
|
||||
|
||||
// Ensure required audience value of the client_id exists.
|
||||
if !utils.IsStringInSlice(requester.GetClient().GetID(), session.Claims.Audience) {
|
||||
session.Claims.Audience = append(session.Claims.Audience, requester.GetClient().GetID())
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
// OpenIDConnectProvider for OpenID Connect.
|
||||
|
@ -74,33 +84,34 @@ type OpenIDConnectProvider struct {
|
|||
discovery OpenIDConnectWellKnownConfiguration
|
||||
}
|
||||
|
||||
// OpenIDConnectStore is Authelia's internal representation of the fosite.Storage interface.
|
||||
//
|
||||
// Currently it is mostly just implementing a decorator pattern other then GetInternalClient.
|
||||
// The long term plan is to have these methods interact with the Authelia storage and
|
||||
// session providers where applicable.
|
||||
// OpenIDConnectStore is Authelia's internal representation of the fosite.Storage interface. It maps the following
|
||||
// interfaces to the storage.Provider interface:
|
||||
// fosite.Storage, fosite.ClientManager, storage.Transactional, oauth2.AuthorizeCodeStorage, oauth2.AccessTokenStorage,
|
||||
// oauth2.RefreshTokenStorage, oauth2.TokenRevocationStorage, pkce.PKCERequestStorage,
|
||||
// openid.OpenIDConnectRequestStorage, and partially implements rfc7523.RFC7523KeyStorage.
|
||||
type OpenIDConnectStore struct {
|
||||
clients map[string]*InternalClient
|
||||
memory *storage.MemoryStore
|
||||
provider storage.Provider
|
||||
clients map[string]*Client
|
||||
}
|
||||
|
||||
// InternalClient represents the client internally.
|
||||
type InternalClient struct {
|
||||
ID string `json:"id"`
|
||||
Description string `json:"-"`
|
||||
Secret []byte `json:"client_secret,omitempty"`
|
||||
Public bool `json:"public"`
|
||||
// Client represents the client internally.
|
||||
type Client struct {
|
||||
ID string
|
||||
SectorIdentifier string
|
||||
Description string
|
||||
Secret []byte
|
||||
Public bool
|
||||
|
||||
Policy authorization.Level `json:"-"`
|
||||
Policy authorization.Level
|
||||
|
||||
Audience []string `json:"audience"`
|
||||
Scopes []string `json:"scopes"`
|
||||
RedirectURIs []string `json:"redirect_uris"`
|
||||
GrantTypes []string `json:"grant_types"`
|
||||
ResponseTypes []string `json:"response_types"`
|
||||
ResponseModes []fosite.ResponseModeType `json:"response_modes"`
|
||||
Audience []string
|
||||
Scopes []string
|
||||
RedirectURIs []string
|
||||
GrantTypes []string
|
||||
ResponseTypes []string
|
||||
ResponseModes []fosite.ResponseModeType
|
||||
|
||||
UserinfoSigningAlgorithm string `json:"userinfo_signed_response_alg,omitempty"`
|
||||
UserinfoSigningAlgorithm string
|
||||
}
|
||||
|
||||
// KeyManager keeps track of all of the active/inactive rsa keys and provides them to services requiring them.
|
||||
|
@ -112,8 +123,8 @@ type KeyManager struct {
|
|||
strategy *RS256JWTStrategy
|
||||
}
|
||||
|
||||
// AutheliaHasher implements the fosite.Hasher interface without an actual hashing algo.
|
||||
type AutheliaHasher struct{}
|
||||
// PlainTextHasher implements the fosite.Hasher interface without an actual hashing algo.
|
||||
type PlainTextHasher struct{}
|
||||
|
||||
// ConsentGetResponseBody schema of the response body of the consent GET endpoint.
|
||||
type ConsentGetResponseBody struct {
|
||||
|
@ -123,12 +134,15 @@ type ConsentGetResponseBody struct {
|
|||
Audience []string `json:"audience"`
|
||||
}
|
||||
|
||||
// OpenIDSession holds OIDC Session information.
|
||||
type OpenIDSession struct {
|
||||
*openid.DefaultSession `json:"idToken"`
|
||||
// ConsentPostRequestBody schema of the request body of the consent POST endpoint.
|
||||
type ConsentPostRequestBody struct {
|
||||
ClientID string `json:"client_id"`
|
||||
AcceptOrReject string `json:"accept_or_reject"`
|
||||
}
|
||||
|
||||
Extra map[string]interface{} `json:"extra"`
|
||||
ClientID string
|
||||
// ConsentPostResponseBody schema of the response body of the consent POST endpoint.
|
||||
type ConsentPostResponseBody struct {
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -9,6 +9,8 @@ import (
|
|||
"github.com/ory/fosite"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
)
|
||||
|
||||
func TestNewSession(t *testing.T) {
|
||||
|
@ -38,7 +40,7 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {
|
|||
Request: fosite.Request{
|
||||
ID: requestID.String(),
|
||||
Form: formValues,
|
||||
Client: &InternalClient{ID: "example"},
|
||||
Client: &Client{ID: "example"},
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -51,7 +53,13 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {
|
|||
issuer := "https://example.com"
|
||||
amr := []string{AMRPasswordBasedAuthentication}
|
||||
|
||||
session := NewSessionWithAuthorizeRequest(issuer, "primary", subject.String(), "john", amr, extra, authAt, requested, request)
|
||||
consent := &model.OAuth2ConsentSession{
|
||||
ChallengeID: uuid.New(),
|
||||
RequestedAt: requested,
|
||||
Subject: subject,
|
||||
}
|
||||
|
||||
session := NewSessionWithAuthorizeRequest(issuer, "primary", "john", amr, extra, authAt, consent, request)
|
||||
|
||||
require.NotNil(t, session)
|
||||
require.NotNil(t, session.Extra)
|
||||
|
@ -78,7 +86,12 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {
|
|||
|
||||
require.Contains(t, session.Claims.Extra, "preferred_username")
|
||||
|
||||
session = NewSessionWithAuthorizeRequest(issuer, "primary", subject.String(), "john", nil, nil, authAt, requested, request)
|
||||
consent = &model.OAuth2ConsentSession{
|
||||
ChallengeID: uuid.New(),
|
||||
RequestedAt: requested,
|
||||
}
|
||||
|
||||
session = NewSessionWithAuthorizeRequest(issuer, "primary", "john", nil, nil, authAt, consent, request)
|
||||
|
||||
require.NotNil(t, session)
|
||||
require.NotNil(t, session.Claims)
|
||||
|
|
|
@ -7,11 +7,11 @@ import (
|
|||
"github.com/fasthttp/session/v2"
|
||||
"github.com/fasthttp/session/v2/providers/redis"
|
||||
"github.com/go-webauthn/webauthn/webauthn"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/authentication"
|
||||
"github.com/authelia/authelia/v4/internal/logging"
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
"github.com/authelia/authelia/v4/internal/oidc"
|
||||
)
|
||||
|
||||
|
@ -43,8 +43,8 @@ type UserSession struct {
|
|||
// Webauthn holds the session registration data for this session.
|
||||
Webauthn *webauthn.SessionData
|
||||
|
||||
// Represent an OIDC workflow session initiated by the client if not null.
|
||||
OIDCWorkflowSession *model.OIDCWorkflowSession
|
||||
// ConsentChallengeID is the OpenID Connect Consent Session challenge ID.
|
||||
ConsentChallengeID *uuid.UUID
|
||||
|
||||
// This boolean is set to true after identity verification and checked
|
||||
// while doing the query actually updating the password.
|
||||
|
|
|
@ -5,18 +5,40 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
tableUserPreferences = "user_preferences"
|
||||
tableAuthenticationLogs = "authentication_logs"
|
||||
tableDuoDevices = "duo_devices"
|
||||
tableIdentityVerification = "identity_verification"
|
||||
tableTOTPConfigurations = "totp_configurations"
|
||||
tableUserOpaqueIdentifier = "user_opaque_identifier"
|
||||
tableUserPreferences = "user_preferences"
|
||||
tableWebauthnDevices = "webauthn_devices"
|
||||
tableDuoDevices = "duo_devices"
|
||||
tableAuthenticationLogs = "authentication_logs"
|
||||
|
||||
tableOAuth2ConsentSession = "oauth2_consent_session"
|
||||
tableOAuth2AuthorizeCodeSession = "oauth2_authorization_code_session"
|
||||
tableOAuth2AccessTokenSession = "oauth2_access_token_session" //nolint:gosec // This is not a hardcoded credential.
|
||||
tableOAuth2RefreshTokenSession = "oauth2_refresh_token_session" //nolint:gosec // This is not a hardcoded credential.
|
||||
tableOAuth2PKCERequestSession = "oauth2_pkce_request_session"
|
||||
tableOAuth2OpenIDConnectSession = "oauth2_openid_connect_session"
|
||||
tableOAuth2BlacklistedJTI = "oauth2_blacklisted_jti"
|
||||
|
||||
tableMigrations = "migrations"
|
||||
tableEncryption = "encryption"
|
||||
|
||||
tablePrefixBackup = "_bkp_"
|
||||
)
|
||||
|
||||
// OAuth2SessionType represents the potential OAuth 2.0 session types.
|
||||
type OAuth2SessionType string
|
||||
|
||||
// Representation of specific OAuth 2.0 session types.
|
||||
const (
|
||||
OAuth2SessionTypeAuthorizeCode OAuth2SessionType = "authorization code"
|
||||
OAuth2SessionTypeAccessToken OAuth2SessionType = "access token"
|
||||
OAuth2SessionTypeRefreshToken OAuth2SessionType = "refresh token"
|
||||
OAuth2SessionTypePKCEChallenge OAuth2SessionType = "pkce challenge"
|
||||
OAuth2SessionTypeOpenIDConnect OAuth2SessionType = "openid connect"
|
||||
)
|
||||
|
||||
const (
|
||||
encryptionNameCheck = "check"
|
||||
)
|
||||
|
@ -56,7 +78,7 @@ const (
|
|||
|
||||
const (
|
||||
// This is the latest schema version for the purpose of tests.
|
||||
testLatestVersion = 3
|
||||
testLatestVersion = 4
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -64,6 +86,12 @@ const (
|
|||
SchemaLatest = 2147483647
|
||||
)
|
||||
|
||||
type ctxKey int
|
||||
|
||||
const (
|
||||
ctxKeyTransaction ctxKey = iota
|
||||
)
|
||||
|
||||
var (
|
||||
reMigration = regexp.MustCompile(`^V(\d{4})\.([^.]+)\.(all|sqlite|postgres|mysql)\.(up|down)\.sql$`)
|
||||
)
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
CREATE TABLE IF NOT EXISTS user_opaque_identifier (
|
||||
id INTEGER AUTO_INCREMENT,
|
||||
service VARCHAR(20) NOT NULL,
|
||||
sector_id VARCHAR(255) NOT NULL,
|
||||
username VARCHAR(100) NOT NULL,
|
||||
identifier CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX user_opaque_identifier_service_sector_id_username_key ON user_opaque_identifier (service, sector_id, username);
|
||||
CREATE UNIQUE INDEX user_opaque_identifier_identifier_key ON user_opaque_identifier (identifier);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_blacklisted_jti (
|
||||
id INTEGER AUTO_INCREMENT,
|
||||
signature VARCHAR(64) NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX oauth2_blacklisted_jti_signature_key ON oauth2_blacklisted_jti (signature);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_consent_session (
|
||||
id INTEGER AUTO_INCREMENT,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
authorized BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
granted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
responded_at TIMESTAMP NULL DEFAULT NULL,
|
||||
expires_at TIMESTAMP NULL DEFAULT NULL,
|
||||
form_data TEXT NOT NULL,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL,
|
||||
granted_audience TEXT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_consent_subject_fkey
|
||||
FOREIGN KEY (subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX oauth2_consent_session_challenge_id_key ON oauth2_consent_session (challenge_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_authorization_code_session (
|
||||
id INTEGER AUTO_INCREMENT,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL,
|
||||
granted_audience TEXT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BLOB NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_authorization_code_session_challenge_id_fkey
|
||||
FOREIGN KEY (challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_authorization_code_session_subject_fkey
|
||||
FOREIGN KEY (subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_authorization_code_session_request_id_idx ON oauth2_authorization_code_session (request_id);
|
||||
CREATE INDEX oauth2_authorization_code_session_client_id_idx ON oauth2_authorization_code_session (client_id);
|
||||
CREATE INDEX oauth2_authorization_code_session_client_id_subject_idx ON oauth2_authorization_code_session (client_id, subject);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_access_token_session (
|
||||
id INTEGER AUTO_INCREMENT,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL,
|
||||
granted_audience TEXT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BLOB NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_access_token_session_challenge_id_fkey
|
||||
FOREIGN KEY(challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_access_token_session_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_access_token_session_request_id_idx ON oauth2_access_token_session (request_id);
|
||||
CREATE INDEX oauth2_access_token_session_client_id_idx ON oauth2_access_token_session (client_id);
|
||||
CREATE INDEX oauth2_access_token_session_client_id_subject_idx ON oauth2_access_token_session (client_id, subject);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_refresh_token_session (
|
||||
id INTEGER AUTO_INCREMENT,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL,
|
||||
granted_audience TEXT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BLOB NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_refresh_token_session_challenge_id_fkey
|
||||
FOREIGN KEY(challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_refresh_token_session_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_refresh_token_session_request_id_idx ON oauth2_refresh_token_session (request_id);
|
||||
CREATE INDEX oauth2_refresh_token_session_client_id_idx ON oauth2_refresh_token_session (client_id);
|
||||
CREATE INDEX oauth2_refresh_token_session_client_id_subject_idx ON oauth2_refresh_token_session (client_id, subject);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_pkce_request_session (
|
||||
id INTEGER AUTO_INCREMENT,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL,
|
||||
granted_audience TEXT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BLOB NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_pkce_request_session_challenge_id_fkey
|
||||
FOREIGN KEY(challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_pkce_request_session_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_pkce_request_session_request_id_idx ON oauth2_pkce_request_session (request_id);
|
||||
CREATE INDEX oauth2_pkce_request_session_client_id_idx ON oauth2_pkce_request_session (client_id);
|
||||
CREATE INDEX oauth2_pkce_request_session_client_id_subject_idx ON oauth2_pkce_request_session (client_id, subject);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_openid_connect_session (
|
||||
id INTEGER AUTO_INCREMENT,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL,
|
||||
granted_audience TEXT NULL,
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BLOB NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_openid_connect_session_challenge_id_fkey
|
||||
FOREIGN KEY(challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_openid_connect_session_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_openid_connect_session_request_id_idx ON oauth2_openid_connect_session (request_id);
|
||||
CREATE INDEX oauth2_openid_connect_session_client_id_idx ON oauth2_openid_connect_session (client_id);
|
||||
CREATE INDEX oauth2_openid_connect_session_client_id_subject_idx ON oauth2_openid_connect_session (client_id, subject);
|
|
@ -0,0 +1,188 @@
|
|||
CREATE TABLE IF NOT EXISTS user_opaque_identifier (
|
||||
id SERIAL,
|
||||
service VARCHAR(20) NOT NULL,
|
||||
sector_id VARCHAR(255) NOT NULL,
|
||||
username VARCHAR(100) NOT NULL,
|
||||
identifier CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX user_opaque_identifier_service_sector_id_username_key ON user_opaque_identifier (service, sector_id, username);
|
||||
CREATE UNIQUE INDEX user_opaque_identifier_identifier_key ON user_opaque_identifier (identifier);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_blacklisted_jti (
|
||||
id SERIAL,
|
||||
signature VARCHAR(64) NOT NULL,
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX oauth2_blacklisted_jti_signature_key ON oauth2_blacklisted_jti (signature);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_consent_session (
|
||||
id SERIAL,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
authorized BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
granted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
responded_at TIMESTAMP WITH TIME ZONE NULL DEFAULT NULL,
|
||||
expires_at TIMESTAMP WITH TIME ZONE NULL DEFAULT NULL,
|
||||
form_data TEXT NOT NULL,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL DEFAULT '',
|
||||
granted_audience TEXT NULL DEFAULT '',
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_consent_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX oauth2_consent_session_challenge_id_key ON oauth2_consent_session (challenge_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_authorization_code_session (
|
||||
id SERIAL,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36),
|
||||
requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL DEFAULT '',
|
||||
granted_audience TEXT NULL DEFAULT '',
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BYTEA NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_authorization_code_session_challenge_id_fkey
|
||||
FOREIGN KEY(challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_authorization_code_session_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_authorization_code_session_request_id_idx ON oauth2_authorization_code_session (request_id);
|
||||
CREATE INDEX oauth2_authorization_code_session_client_id_idx ON oauth2_authorization_code_session (client_id);
|
||||
CREATE INDEX oauth2_authorization_code_session_client_id_subject_idx ON oauth2_authorization_code_session (client_id, subject);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_access_token_session (
|
||||
id SERIAL,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL DEFAULT '',
|
||||
granted_audience TEXT NULL DEFAULT '',
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BYTEA NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_access_token_session_challenge_id_fkey
|
||||
FOREIGN KEY(challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_access_token_session_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_access_token_session_request_id_idx ON oauth2_access_token_session (request_id);
|
||||
CREATE INDEX oauth2_access_token_session_client_id_idx ON oauth2_access_token_session (client_id);
|
||||
CREATE INDEX oauth2_access_token_session_client_id_subject_idx ON oauth2_access_token_session (client_id, subject);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_refresh_token_session (
|
||||
id SERIAL,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL DEFAULT '',
|
||||
granted_audience TEXT NULL DEFAULT '',
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BYTEA NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_refresh_token_session_challenge_id_fkey
|
||||
FOREIGN KEY(challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_refresh_token_session_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_refresh_token_session_request_id_idx ON oauth2_refresh_token_session (request_id);
|
||||
CREATE INDEX oauth2_refresh_token_session_client_id_idx ON oauth2_refresh_token_session (client_id);
|
||||
CREATE INDEX oauth2_refresh_token_session_client_id_subject_idx ON oauth2_refresh_token_session (client_id, subject);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_pkce_request_session (
|
||||
id SERIAL,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL DEFAULT '',
|
||||
granted_audience TEXT NULL DEFAULT '',
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BYTEA NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_pkce_request_session_challenge_id_fkey
|
||||
FOREIGN KEY(challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_pkce_request_session_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_pkce_request_session_request_id_idx ON oauth2_pkce_request_session (request_id);
|
||||
CREATE INDEX oauth2_pkce_request_session_client_id_idx ON oauth2_pkce_request_session (client_id);
|
||||
CREATE INDEX oauth2_pkce_request_session_client_id_subject_idx ON oauth2_pkce_request_session (client_id, subject);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_openid_connect_session (
|
||||
id SERIAL,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
requested_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL DEFAULT '',
|
||||
granted_audience TEXT NULL DEFAULT '',
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BYTEA NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_openid_connect_session_challenge_id_fkey
|
||||
FOREIGN KEY(challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_openid_connect_session_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_openid_connect_session_request_id_idx ON oauth2_openid_connect_session (request_id);
|
||||
CREATE INDEX oauth2_openid_connect_session_client_id_idx ON oauth2_openid_connect_session (client_id);
|
||||
CREATE INDEX oauth2_openid_connect_session_client_id_subject_idx ON oauth2_openid_connect_session (client_id, subject);
|
|
@ -0,0 +1,188 @@
|
|||
CREATE TABLE IF NOT EXISTS user_opaque_identifier (
|
||||
id INTEGER,
|
||||
service VARCHAR(20) NOT NULL,
|
||||
sector_id VARCHAR(255) NOT NULL,
|
||||
username VARCHAR(100) NOT NULL,
|
||||
identifier CHAR(36) NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX user_opaque_identifier_service_sector_id_username_key ON user_opaque_identifier (service, sector_id, username);
|
||||
CREATE UNIQUE INDEX user_opaque_identifier_identifier_key ON user_opaque_identifier (identifier);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_blacklisted_jti (
|
||||
id INTEGER,
|
||||
signature VARCHAR(64) NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX oauth2_blacklisted_jti_signature_key ON oauth2_blacklisted_jti (signature);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_consent_session (
|
||||
id INTEGER,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
authorized BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
granted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
responded_at TIMESTAMP NULL DEFAULT NULL,
|
||||
expires_at TIMESTAMP NULL DEFAULT NULL,
|
||||
form_data TEXT NOT NULL,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL DEFAULT '',
|
||||
granted_audience TEXT NULL DEFAULT '',
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_consent_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX oauth2_consent_session_challenge_id_key ON oauth2_consent_session (challenge_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_authorization_code_session (
|
||||
id INTEGER,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL DEFAULT '',
|
||||
granted_audience TEXT NULL DEFAULT '',
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BLOB NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_authorization_code_session_challenge_id_fkey
|
||||
FOREIGN KEY(challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_authorization_code_session_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_authorization_code_session_request_id_idx ON oauth2_authorization_code_session (request_id);
|
||||
CREATE INDEX oauth2_authorization_code_session_client_id_idx ON oauth2_authorization_code_session (client_id);
|
||||
CREATE INDEX oauth2_authorization_code_session_client_id_subject_idx ON oauth2_authorization_code_session (client_id, subject);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_access_token_session (
|
||||
id INTEGER,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL DEFAULT '',
|
||||
granted_audience TEXT NULL DEFAULT '',
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BLOB NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_access_token_session_challenge_id_fkey
|
||||
FOREIGN KEY(challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_access_token_session_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_access_token_session_request_id_idx ON oauth2_access_token_session (request_id);
|
||||
CREATE INDEX oauth2_access_token_session_client_id_idx ON oauth2_access_token_session (client_id);
|
||||
CREATE INDEX oauth2_access_token_session_client_id_subject_idx ON oauth2_access_token_session (client_id, subject);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_refresh_token_session (
|
||||
id INTEGER,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL DEFAULT '',
|
||||
granted_audience TEXT NULL DEFAULT '',
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BLOB NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_refresh_token_session_challenge_id_fkey
|
||||
FOREIGN KEY(challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_refresh_token_session_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_refresh_token_session_request_id_idx ON oauth2_refresh_token_session (request_id);
|
||||
CREATE INDEX oauth2_refresh_token_session_client_id_idx ON oauth2_refresh_token_session (client_id);
|
||||
CREATE INDEX oauth2_refresh_token_session_client_id_subject_idx ON oauth2_refresh_token_session (client_id, subject);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_pkce_request_session (
|
||||
id INTEGER,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL DEFAULT '',
|
||||
granted_audience TEXT NULL DEFAULT '',
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BLOB NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_pkce_request_session_challenge_id_fkey
|
||||
FOREIGN KEY(challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_pkce_request_session_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_pkce_request_session_request_id_idx ON oauth2_pkce_request_session (request_id);
|
||||
CREATE INDEX oauth2_pkce_request_session_client_id_idx ON oauth2_pkce_request_session (client_id);
|
||||
CREATE INDEX oauth2_pkce_request_session_client_id_subject_idx ON oauth2_pkce_request_session (client_id, subject);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth2_openid_connect_session (
|
||||
id INTEGER,
|
||||
challenge_id CHAR(36) NOT NULL,
|
||||
request_id VARCHAR(40) NOT NULL,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
signature VARCHAR(255) NOT NULL,
|
||||
subject CHAR(36) NOT NULL,
|
||||
requested_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
requested_scopes TEXT NOT NULL,
|
||||
granted_scopes TEXT NOT NULL,
|
||||
requested_audience TEXT NULL DEFAULT '',
|
||||
granted_audience TEXT NULL DEFAULT '',
|
||||
active BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
form_data TEXT NOT NULL,
|
||||
session_data BLOB NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT oauth2_openid_connect_session_challenge_id_fkey
|
||||
FOREIGN KEY(challenge_id)
|
||||
REFERENCES oauth2_consent_session(challenge_id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT oauth2_openid_connect_session_subject_fkey
|
||||
FOREIGN KEY(subject)
|
||||
REFERENCES user_opaque_identifier(identifier) ON UPDATE RESTRICT ON DELETE RESTRICT
|
||||
);
|
||||
|
||||
CREATE INDEX oauth2_openid_connect_session_request_id_idx ON oauth2_openid_connect_session (request_id);
|
||||
CREATE INDEX oauth2_openid_connect_session_client_id_idx ON oauth2_openid_connect_session (client_id);
|
||||
CREATE INDEX oauth2_openid_connect_session_client_id_subject_idx ON oauth2_openid_connect_session (client_id, subject);
|
|
@ -0,0 +1,8 @@
|
|||
DROP TABLE IF EXISTS oauth2_blacklisted_jti;
|
||||
DROP TABLE IF EXISTS oauth2_authorization_code_session;
|
||||
DROP TABLE IF EXISTS oauth2_access_token_session;
|
||||
DROP TABLE IF EXISTS oauth2_refresh_token_session;
|
||||
DROP TABLE IF EXISTS oauth2_pkce_request_session;
|
||||
DROP TABLE IF EXISTS oauth2_openid_connect_session;
|
||||
DROP TABLE IF EXISTS oauth2_consent_session;
|
||||
DROP TABLE IF EXISTS user_opaque_identifier;
|
|
@ -4,6 +4,9 @@ import (
|
|||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ory/fosite/storage"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
)
|
||||
|
||||
|
@ -13,10 +16,16 @@ type Provider interface {
|
|||
|
||||
RegulatorProvider
|
||||
|
||||
storage.Transactional
|
||||
|
||||
SavePreferred2FAMethod(ctx context.Context, username string, method string) (err error)
|
||||
LoadPreferred2FAMethod(ctx context.Context, username string) (method string, err error)
|
||||
LoadUserInfo(ctx context.Context, username string) (info model.UserInfo, err error)
|
||||
|
||||
SaveUserOpaqueIdentifier(ctx context.Context, subject model.UserOpaqueIdentifier) (err error)
|
||||
LoadUserOpaqueIdentifier(ctx context.Context, opaqueUUID uuid.UUID) (subject *model.UserOpaqueIdentifier, err error)
|
||||
LoadUserOpaqueIdentifierBySignature(ctx context.Context, service, sectorID, username string) (subject *model.UserOpaqueIdentifier, err error)
|
||||
|
||||
SaveIdentityVerification(ctx context.Context, verification model.IdentityVerification) (err error)
|
||||
ConsumeIdentityVerification(ctx context.Context, jti string, ip model.NullIP) (err error)
|
||||
FindIdentityVerification(ctx context.Context, jti string) (found bool, err error)
|
||||
|
@ -36,6 +45,22 @@ type Provider interface {
|
|||
DeletePreferredDuoDevice(ctx context.Context, username string) (err error)
|
||||
LoadPreferredDuoDevice(ctx context.Context, username string) (device *model.DuoDevice, err error)
|
||||
|
||||
SaveOAuth2ConsentSession(ctx context.Context, consent model.OAuth2ConsentSession) (err error)
|
||||
SaveOAuth2ConsentSessionResponse(ctx context.Context, consent model.OAuth2ConsentSession, rejection bool) (err error)
|
||||
SaveOAuth2ConsentSessionGranted(ctx context.Context, id int) (err error)
|
||||
LoadOAuth2ConsentSessionByChallengeID(ctx context.Context, challengeID uuid.UUID) (consent *model.OAuth2ConsentSession, err error)
|
||||
LoadOAuth2ConsentSessionsPreConfigured(ctx context.Context, clientID string, subject uuid.UUID) (rows *ConsentSessionRows, err error)
|
||||
|
||||
SaveOAuth2Session(ctx context.Context, sessionType OAuth2SessionType, session model.OAuth2Session) (err error)
|
||||
RevokeOAuth2Session(ctx context.Context, sessionType OAuth2SessionType, signature string) (err error)
|
||||
RevokeOAuth2SessionByRequestID(ctx context.Context, sessionType OAuth2SessionType, requestID string) (err error)
|
||||
DeactivateOAuth2Session(ctx context.Context, sessionType OAuth2SessionType, signature string) (err error)
|
||||
DeactivateOAuth2SessionByRequestID(ctx context.Context, sessionType OAuth2SessionType, requestID string) (err error)
|
||||
LoadOAuth2Session(ctx context.Context, sessionType OAuth2SessionType, signature string) (session *model.OAuth2Session, err error)
|
||||
|
||||
SaveOAuth2BlacklistedJTI(ctx context.Context, blacklistedJTI model.OAuth2BlacklistedJTI) (err error)
|
||||
LoadOAuth2BlacklistedJTI(ctx context.Context, signature string) (blacklistedJTI *model.OAuth2BlacklistedJTI, err error)
|
||||
|
||||
SchemaTables(ctx context.Context) (tables []string, err error)
|
||||
SchemaVersion(ctx context.Context) (version int, err error)
|
||||
SchemaLatestVersion() (version int, err error)
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
|
@ -63,6 +64,54 @@ func NewSQLProvider(config *schema.Configuration, name, driverName, dataSourceNa
|
|||
sqlSelectPreferred2FAMethod: fmt.Sprintf(queryFmtSelectPreferred2FAMethod, tableUserPreferences),
|
||||
sqlSelectUserInfo: fmt.Sprintf(queryFmtSelectUserInfo, tableTOTPConfigurations, tableWebauthnDevices, tableDuoDevices, tableUserPreferences),
|
||||
|
||||
sqlInsertUserOpaqueIdentifier: fmt.Sprintf(queryFmtInsertUserOpaqueIdentifier, tableUserOpaqueIdentifier),
|
||||
sqlSelectUserOpaqueIdentifier: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifier, tableUserOpaqueIdentifier),
|
||||
sqlSelectUserOpaqueIdentifierBySignature: fmt.Sprintf(queryFmtSelectUserOpaqueIdentifierBySignature, tableUserOpaqueIdentifier),
|
||||
|
||||
sqlInsertOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2AuthorizeCodeSession),
|
||||
sqlSelectOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2AuthorizeCodeSession),
|
||||
sqlRevokeOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2AuthorizeCodeSession),
|
||||
sqlRevokeOAuth2AuthorizeCodeSessionByRequestID: fmt.Sprintf(queryFmtRevokeOAuth2SessionByRequestID, tableOAuth2AuthorizeCodeSession),
|
||||
sqlDeactivateOAuth2AuthorizeCodeSession: fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2AuthorizeCodeSession),
|
||||
sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2AuthorizeCodeSession),
|
||||
|
||||
sqlInsertOAuth2AccessTokenSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2AccessTokenSession),
|
||||
sqlSelectOAuth2AccessTokenSession: fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2AccessTokenSession),
|
||||
sqlRevokeOAuth2AccessTokenSession: fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2AccessTokenSession),
|
||||
sqlRevokeOAuth2AccessTokenSessionByRequestID: fmt.Sprintf(queryFmtRevokeOAuth2SessionByRequestID, tableOAuth2AccessTokenSession),
|
||||
sqlDeactivateOAuth2AccessTokenSession: fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2AccessTokenSession),
|
||||
sqlDeactivateOAuth2AccessTokenSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2AccessTokenSession),
|
||||
|
||||
sqlInsertOAuth2RefreshTokenSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2RefreshTokenSession),
|
||||
sqlSelectOAuth2RefreshTokenSession: fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2RefreshTokenSession),
|
||||
sqlRevokeOAuth2RefreshTokenSession: fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2RefreshTokenSession),
|
||||
sqlRevokeOAuth2RefreshTokenSessionByRequestID: fmt.Sprintf(queryFmtRevokeOAuth2SessionByRequestID, tableOAuth2RefreshTokenSession),
|
||||
sqlDeactivateOAuth2RefreshTokenSession: fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2RefreshTokenSession),
|
||||
sqlDeactivateOAuth2RefreshTokenSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2RefreshTokenSession),
|
||||
|
||||
sqlInsertOAuth2PKCERequestSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2PKCERequestSession),
|
||||
sqlSelectOAuth2PKCERequestSession: fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2PKCERequestSession),
|
||||
sqlRevokeOAuth2PKCERequestSession: fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2PKCERequestSession),
|
||||
sqlRevokeOAuth2PKCERequestSessionByRequestID: fmt.Sprintf(queryFmtRevokeOAuth2SessionByRequestID, tableOAuth2PKCERequestSession),
|
||||
sqlDeactivateOAuth2PKCERequestSession: fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2PKCERequestSession),
|
||||
sqlDeactivateOAuth2PKCERequestSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2PKCERequestSession),
|
||||
|
||||
sqlInsertOAuth2OpenIDConnectSession: fmt.Sprintf(queryFmtInsertOAuth2Session, tableOAuth2OpenIDConnectSession),
|
||||
sqlSelectOAuth2OpenIDConnectSession: fmt.Sprintf(queryFmtSelectOAuth2Session, tableOAuth2OpenIDConnectSession),
|
||||
sqlRevokeOAuth2OpenIDConnectSession: fmt.Sprintf(queryFmtRevokeOAuth2Session, tableOAuth2OpenIDConnectSession),
|
||||
sqlRevokeOAuth2OpenIDConnectSessionByRequestID: fmt.Sprintf(queryFmtRevokeOAuth2SessionByRequestID, tableOAuth2OpenIDConnectSession),
|
||||
sqlDeactivateOAuth2OpenIDConnectSession: fmt.Sprintf(queryFmtDeactivateOAuth2Session, tableOAuth2OpenIDConnectSession),
|
||||
sqlDeactivateOAuth2OpenIDConnectSessionByRequestID: fmt.Sprintf(queryFmtDeactivateOAuth2SessionByRequestID, tableOAuth2OpenIDConnectSession),
|
||||
|
||||
sqlInsertOAuth2ConsentSession: fmt.Sprintf(queryFmtInsertOAuth2ConsentSession, tableOAuth2ConsentSession),
|
||||
sqlUpdateOAuth2ConsentSessionResponse: fmt.Sprintf(queryFmtUpdateOAuth2ConsentSessionResponse, tableOAuth2ConsentSession),
|
||||
sqlUpdateOAuth2ConsentSessionGranted: fmt.Sprintf(queryFmtUpdateOAuth2ConsentSessionGranted, tableOAuth2ConsentSession),
|
||||
sqlSelectOAuth2ConsentSessionByChallengeID: fmt.Sprintf(queryFmtSelectOAuth2ConsentSessionByChallengeID, tableOAuth2ConsentSession),
|
||||
sqlSelectOAuth2ConsentSessionsPreConfigured: fmt.Sprintf(queryFmtSelectOAuth2ConsentSessionsPreConfigured, tableOAuth2ConsentSession),
|
||||
|
||||
sqlUpsertOAuth2BlacklistedJTI: fmt.Sprintf(queryFmtUpsertOAuth2BlacklistedJTI, tableOAuth2BlacklistedJTI),
|
||||
sqlSelectOAuth2BlacklistedJTI: fmt.Sprintf(queryFmtSelectOAuth2BlacklistedJTI, tableOAuth2BlacklistedJTI),
|
||||
|
||||
sqlInsertMigration: fmt.Sprintf(queryFmtInsertMigration, tableMigrations),
|
||||
sqlSelectMigrations: fmt.Sprintf(queryFmtSelectMigrations, tableMigrations),
|
||||
sqlSelectLatestMigration: fmt.Sprintf(queryFmtSelectLatestMigration, tableMigrations),
|
||||
|
@ -128,6 +177,11 @@ type SQLProvider struct {
|
|||
sqlSelectPreferred2FAMethod string
|
||||
sqlSelectUserInfo string
|
||||
|
||||
// Table: user_opaque_identifier.
|
||||
sqlInsertUserOpaqueIdentifier string
|
||||
sqlSelectUserOpaqueIdentifier string
|
||||
sqlSelectUserOpaqueIdentifierBySignature string
|
||||
|
||||
// Table: migrations.
|
||||
sqlInsertMigration string
|
||||
sqlSelectMigrations string
|
||||
|
@ -137,6 +191,56 @@ type SQLProvider struct {
|
|||
sqlUpsertEncryptionValue string
|
||||
sqlSelectEncryptionValue string
|
||||
|
||||
// Table: oauth2_authorization_code_session.
|
||||
sqlInsertOAuth2AuthorizeCodeSession string
|
||||
sqlSelectOAuth2AuthorizeCodeSession string
|
||||
sqlRevokeOAuth2AuthorizeCodeSession string
|
||||
sqlRevokeOAuth2AuthorizeCodeSessionByRequestID string
|
||||
sqlDeactivateOAuth2AuthorizeCodeSession string
|
||||
sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID string
|
||||
|
||||
// Table: oauth2_access_token_session.
|
||||
sqlInsertOAuth2AccessTokenSession string
|
||||
sqlSelectOAuth2AccessTokenSession string
|
||||
sqlRevokeOAuth2AccessTokenSession string
|
||||
sqlRevokeOAuth2AccessTokenSessionByRequestID string
|
||||
sqlDeactivateOAuth2AccessTokenSession string
|
||||
sqlDeactivateOAuth2AccessTokenSessionByRequestID string
|
||||
|
||||
// Table: oauth2_refresh_token_session.
|
||||
sqlInsertOAuth2RefreshTokenSession string
|
||||
sqlSelectOAuth2RefreshTokenSession string
|
||||
sqlRevokeOAuth2RefreshTokenSession string
|
||||
sqlRevokeOAuth2RefreshTokenSessionByRequestID string
|
||||
sqlDeactivateOAuth2RefreshTokenSession string
|
||||
sqlDeactivateOAuth2RefreshTokenSessionByRequestID string
|
||||
|
||||
// Table: oauth2_pkce_request_session.
|
||||
sqlInsertOAuth2PKCERequestSession string
|
||||
sqlSelectOAuth2PKCERequestSession string
|
||||
sqlRevokeOAuth2PKCERequestSession string
|
||||
sqlRevokeOAuth2PKCERequestSessionByRequestID string
|
||||
sqlDeactivateOAuth2PKCERequestSession string
|
||||
sqlDeactivateOAuth2PKCERequestSessionByRequestID string
|
||||
|
||||
// Table: oauth2_openid_connect_session.
|
||||
sqlInsertOAuth2OpenIDConnectSession string
|
||||
sqlSelectOAuth2OpenIDConnectSession string
|
||||
sqlRevokeOAuth2OpenIDConnectSession string
|
||||
sqlRevokeOAuth2OpenIDConnectSessionByRequestID string
|
||||
sqlDeactivateOAuth2OpenIDConnectSession string
|
||||
sqlDeactivateOAuth2OpenIDConnectSessionByRequestID string
|
||||
|
||||
// Table: oauth2_consent_session.
|
||||
sqlInsertOAuth2ConsentSession string
|
||||
sqlUpdateOAuth2ConsentSessionResponse string
|
||||
sqlUpdateOAuth2ConsentSessionGranted string
|
||||
sqlSelectOAuth2ConsentSessionByChallengeID string
|
||||
sqlSelectOAuth2ConsentSessionsPreConfigured string
|
||||
|
||||
sqlUpsertOAuth2BlacklistedJTI string
|
||||
sqlSelectOAuth2BlacklistedJTI string
|
||||
|
||||
// Utility.
|
||||
sqlSelectExistingTables string
|
||||
sqlFmtRenameTable string
|
||||
|
@ -187,6 +291,329 @@ func (p *SQLProvider) StartupCheck() (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
// BeginTX begins a transaction.
|
||||
func (p *SQLProvider) BeginTX(ctx context.Context) (c context.Context, err error) {
|
||||
var tx *sql.Tx
|
||||
|
||||
if tx, err = p.db.Begin(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return context.WithValue(ctx, ctxKeyTransaction, tx), nil
|
||||
}
|
||||
|
||||
// Commit performs a database commit.
|
||||
func (p *SQLProvider) Commit(ctx context.Context) (err error) {
|
||||
tx, ok := ctx.Value(ctxKeyTransaction).(*sql.Tx)
|
||||
|
||||
if !ok {
|
||||
return errors.New("could not retrieve tx")
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// Rollback performs a database rollback.
|
||||
func (p *SQLProvider) Rollback(ctx context.Context) (err error) {
|
||||
tx, ok := ctx.Value(ctxKeyTransaction).(*sql.Tx)
|
||||
|
||||
if !ok {
|
||||
return errors.New("could not retrieve tx")
|
||||
}
|
||||
|
||||
return tx.Rollback()
|
||||
}
|
||||
|
||||
// SaveUserOpaqueIdentifier saves a new opaque user identifier to the database.
|
||||
func (p *SQLProvider) SaveUserOpaqueIdentifier(ctx context.Context, opaqueID model.UserOpaqueIdentifier) (err error) {
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlInsertUserOpaqueIdentifier, opaqueID.Service, opaqueID.SectorID, opaqueID.Username, opaqueID.Identifier); err != nil {
|
||||
return fmt.Errorf("error inserting user opaque id for user '%s' with opaque id '%s': %w", opaqueID.Username, opaqueID.Identifier.String(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadUserOpaqueIdentifier selects an opaque user identifier from the database.
|
||||
func (p *SQLProvider) LoadUserOpaqueIdentifier(ctx context.Context, opaqueUUID uuid.UUID) (opaqueID *model.UserOpaqueIdentifier, err error) {
|
||||
opaqueID = &model.UserOpaqueIdentifier{}
|
||||
|
||||
if err = p.db.GetContext(ctx, opaqueID, p.sqlSelectUserOpaqueIdentifier, opaqueUUID); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return opaqueID, nil
|
||||
}
|
||||
|
||||
// LoadUserOpaqueIdentifierBySignature selects an opaque user identifier from the database given a service name, sector id, and username.
|
||||
func (p *SQLProvider) LoadUserOpaqueIdentifierBySignature(ctx context.Context, service, sectorID, username string) (opaqueID *model.UserOpaqueIdentifier, err error) {
|
||||
opaqueID = &model.UserOpaqueIdentifier{}
|
||||
|
||||
if err = p.db.GetContext(ctx, opaqueID, p.sqlSelectUserOpaqueIdentifierBySignature, service, sectorID, username); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return opaqueID, nil
|
||||
}
|
||||
|
||||
// SaveOAuth2ConsentSession inserts an OAuth2.0 consent.
|
||||
func (p *SQLProvider) SaveOAuth2ConsentSession(ctx context.Context, consent model.OAuth2ConsentSession) (err error) {
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlInsertOAuth2ConsentSession,
|
||||
consent.ChallengeID, consent.ClientID, consent.Subject, consent.Authorized, consent.Granted,
|
||||
consent.RequestedAt, consent.RespondedAt, consent.ExpiresAt, consent.Form,
|
||||
consent.RequestedScopes, consent.GrantedScopes, consent.RequestedAudience, consent.GrantedAudience); err != nil {
|
||||
return fmt.Errorf("error inserting oauth2 consent session with challenge id '%s' for subject '%s': %w", consent.ChallengeID.String(), consent.Subject.String(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveOAuth2ConsentSessionResponse updates an OAuth2.0 consent with the consent response.
|
||||
func (p *SQLProvider) SaveOAuth2ConsentSessionResponse(ctx context.Context, consent model.OAuth2ConsentSession, authorized bool) (err error) {
|
||||
_, err = p.db.ExecContext(ctx, p.sqlUpdateOAuth2ConsentSessionResponse, authorized, consent.ExpiresAt, consent.GrantedScopes, consent.GrantedAudience, consent.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating oauth2 consent session (authorized '%t') with id '%d' and challenge id '%s' for subject '%s': %w", authorized, consent.ID, consent.ChallengeID, consent.Subject, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveOAuth2ConsentSessionGranted updates an OAuth2.0 consent recording that it has been granted by the authorization endpoint.
|
||||
func (p *SQLProvider) SaveOAuth2ConsentSessionGranted(ctx context.Context, id int) (err error) {
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlUpdateOAuth2ConsentSessionGranted, id); err != nil {
|
||||
return fmt.Errorf("error updating oauth2 consent session (granted) with id '%d': %w", id, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadOAuth2ConsentSessionByChallengeID returns an OAuth2.0 consent given the challenge ID.
|
||||
func (p *SQLProvider) LoadOAuth2ConsentSessionByChallengeID(ctx context.Context, challengeID uuid.UUID) (consent *model.OAuth2ConsentSession, err error) {
|
||||
consent = &model.OAuth2ConsentSession{}
|
||||
|
||||
if err = p.db.GetContext(ctx, consent, p.sqlSelectOAuth2ConsentSessionByChallengeID, challengeID); err != nil {
|
||||
return nil, fmt.Errorf("error selecting oauth2 consent session with challenge id '%s': %w", challengeID.String(), err)
|
||||
}
|
||||
|
||||
return consent, nil
|
||||
}
|
||||
|
||||
// LoadOAuth2ConsentSessionsPreConfigured returns an OAuth2.0 consents that are pre-configured given the consent signature.
|
||||
func (p *SQLProvider) LoadOAuth2ConsentSessionsPreConfigured(ctx context.Context, clientID string, subject uuid.UUID) (rows *ConsentSessionRows, err error) {
|
||||
var r *sqlx.Rows
|
||||
|
||||
if r, err = p.db.QueryxContext(ctx, p.sqlSelectOAuth2ConsentSessionsPreConfigured, clientID, subject); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return &ConsentSessionRows{}, nil
|
||||
}
|
||||
|
||||
return &ConsentSessionRows{}, fmt.Errorf("error selecting oauth2 consent session by signature with client id '%s' and subject '%s': %w", clientID, subject.String(), err)
|
||||
}
|
||||
|
||||
return &ConsentSessionRows{rows: r}, nil
|
||||
}
|
||||
|
||||
// SaveOAuth2Session saves a OAuth2Session to the database.
|
||||
func (p *SQLProvider) SaveOAuth2Session(ctx context.Context, sessionType OAuth2SessionType, session model.OAuth2Session) (err error) {
|
||||
var query string
|
||||
|
||||
switch sessionType {
|
||||
case OAuth2SessionTypeAuthorizeCode:
|
||||
query = p.sqlInsertOAuth2AuthorizeCodeSession
|
||||
case OAuth2SessionTypeAccessToken:
|
||||
query = p.sqlInsertOAuth2AccessTokenSession
|
||||
case OAuth2SessionTypeRefreshToken:
|
||||
query = p.sqlInsertOAuth2RefreshTokenSession
|
||||
case OAuth2SessionTypePKCEChallenge:
|
||||
query = p.sqlInsertOAuth2PKCERequestSession
|
||||
case OAuth2SessionTypeOpenIDConnect:
|
||||
query = p.sqlInsertOAuth2OpenIDConnectSession
|
||||
default:
|
||||
return fmt.Errorf("error inserting oauth2 session for subject '%s' and request id '%s': unknown oauth2 session type '%s'", session.Subject, session.RequestID, sessionType)
|
||||
}
|
||||
|
||||
if session.Session, err = p.encrypt(session.Session); err != nil {
|
||||
return fmt.Errorf("error encrypting the oauth2 %s session data for subject '%s' and request id '%s' and challenge id '%s': %w", sessionType, session.Subject, session.RequestID, session.ChallengeID.String(), err)
|
||||
}
|
||||
|
||||
_, err = p.db.ExecContext(ctx, query,
|
||||
session.ChallengeID, session.RequestID, session.ClientID, session.Signature,
|
||||
session.Subject, session.RequestedAt, session.RequestedScopes, session.GrantedScopes,
|
||||
session.RequestedAudience, session.GrantedAudience,
|
||||
session.Active, session.Revoked, session.Form, session.Session)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error inserting oauth2 %s session data for subject '%s' and request id '%s' and challenge id '%s': %w", sessionType, session.Subject, session.RequestID, session.ChallengeID.String(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokeOAuth2Session marks a OAuth2Session as revoked in the database.
|
||||
func (p *SQLProvider) RevokeOAuth2Session(ctx context.Context, sessionType OAuth2SessionType, signature string) (err error) {
|
||||
var query string
|
||||
|
||||
switch sessionType {
|
||||
case OAuth2SessionTypeAuthorizeCode:
|
||||
query = p.sqlRevokeOAuth2AuthorizeCodeSession
|
||||
case OAuth2SessionTypeAccessToken:
|
||||
query = p.sqlRevokeOAuth2AccessTokenSession
|
||||
case OAuth2SessionTypeRefreshToken:
|
||||
query = p.sqlRevokeOAuth2RefreshTokenSession
|
||||
case OAuth2SessionTypePKCEChallenge:
|
||||
query = p.sqlRevokeOAuth2PKCERequestSession
|
||||
case OAuth2SessionTypeOpenIDConnect:
|
||||
query = p.sqlRevokeOAuth2OpenIDConnectSession
|
||||
default:
|
||||
return fmt.Errorf("error revoking oauth2 session with signature '%s': unknown oauth2 session type '%s'", signature, sessionType)
|
||||
}
|
||||
|
||||
if _, err = p.db.ExecContext(ctx, query, signature); err != nil {
|
||||
return fmt.Errorf("error revoking oauth2 %s session with signature '%s': %w", sessionType, signature, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokeOAuth2SessionByRequestID marks a OAuth2Session as revoked in the database.
|
||||
func (p *SQLProvider) RevokeOAuth2SessionByRequestID(ctx context.Context, sessionType OAuth2SessionType, requestID string) (err error) {
|
||||
var query string
|
||||
|
||||
switch sessionType {
|
||||
case OAuth2SessionTypeAuthorizeCode:
|
||||
query = p.sqlRevokeOAuth2AuthorizeCodeSessionByRequestID
|
||||
case OAuth2SessionTypeAccessToken:
|
||||
query = p.sqlRevokeOAuth2AccessTokenSessionByRequestID
|
||||
case OAuth2SessionTypeRefreshToken:
|
||||
query = p.sqlRevokeOAuth2RefreshTokenSessionByRequestID
|
||||
case OAuth2SessionTypePKCEChallenge:
|
||||
query = p.sqlRevokeOAuth2PKCERequestSessionByRequestID
|
||||
case OAuth2SessionTypeOpenIDConnect:
|
||||
query = p.sqlRevokeOAuth2OpenIDConnectSessionByRequestID
|
||||
default:
|
||||
return fmt.Errorf("error revoking oauth2 session with request id '%s': unknown oauth2 session type '%s'", requestID, sessionType)
|
||||
}
|
||||
|
||||
if _, err = p.db.ExecContext(ctx, query, requestID); err != nil {
|
||||
return fmt.Errorf("error revoking oauth2 %s session with request id '%s': %w", sessionType, requestID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeactivateOAuth2Session marks a OAuth2Session as inactive in the database.
|
||||
func (p *SQLProvider) DeactivateOAuth2Session(ctx context.Context, sessionType OAuth2SessionType, signature string) (err error) {
|
||||
var query string
|
||||
|
||||
switch sessionType {
|
||||
case OAuth2SessionTypeAuthorizeCode:
|
||||
query = p.sqlDeactivateOAuth2AuthorizeCodeSession
|
||||
case OAuth2SessionTypeAccessToken:
|
||||
query = p.sqlDeactivateOAuth2AccessTokenSession
|
||||
case OAuth2SessionTypeRefreshToken:
|
||||
query = p.sqlDeactivateOAuth2RefreshTokenSession
|
||||
case OAuth2SessionTypePKCEChallenge:
|
||||
query = p.sqlDeactivateOAuth2PKCERequestSession
|
||||
case OAuth2SessionTypeOpenIDConnect:
|
||||
query = p.sqlDeactivateOAuth2OpenIDConnectSession
|
||||
default:
|
||||
return fmt.Errorf("error deactivating oauth2 session with signature '%s': unknown oauth2 session type '%s'", signature, sessionType)
|
||||
}
|
||||
|
||||
if _, err = p.db.ExecContext(ctx, query, signature); err != nil {
|
||||
return fmt.Errorf("error deactivating oauth2 %s session with signature '%s': %w", sessionType, signature, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeactivateOAuth2SessionByRequestID marks a OAuth2Session as inactive in the database.
|
||||
func (p *SQLProvider) DeactivateOAuth2SessionByRequestID(ctx context.Context, sessionType OAuth2SessionType, requestID string) (err error) {
|
||||
var query string
|
||||
|
||||
switch sessionType {
|
||||
case OAuth2SessionTypeAuthorizeCode:
|
||||
query = p.sqlDeactivateOAuth2AuthorizeCodeSession
|
||||
case OAuth2SessionTypeAccessToken:
|
||||
query = p.sqlDeactivateOAuth2AccessTokenSessionByRequestID
|
||||
case OAuth2SessionTypeRefreshToken:
|
||||
query = p.sqlDeactivateOAuth2RefreshTokenSessionByRequestID
|
||||
case OAuth2SessionTypePKCEChallenge:
|
||||
query = p.sqlDeactivateOAuth2PKCERequestSessionByRequestID
|
||||
case OAuth2SessionTypeOpenIDConnect:
|
||||
query = p.sqlDeactivateOAuth2OpenIDConnectSessionByRequestID
|
||||
default:
|
||||
return fmt.Errorf("error deactivating oauth2 session with request id '%s': unknown oauth2 session type '%s'", requestID, sessionType)
|
||||
}
|
||||
|
||||
if _, err = p.db.ExecContext(ctx, query, requestID); err != nil {
|
||||
return fmt.Errorf("error deactivating oauth2 %s session with request id '%s': %w", sessionType, requestID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadOAuth2Session saves a OAuth2Session from the database.
|
||||
func (p *SQLProvider) LoadOAuth2Session(ctx context.Context, sessionType OAuth2SessionType, signature string) (session *model.OAuth2Session, err error) {
|
||||
var query string
|
||||
|
||||
switch sessionType {
|
||||
case OAuth2SessionTypeAuthorizeCode:
|
||||
query = p.sqlSelectOAuth2AuthorizeCodeSession
|
||||
case OAuth2SessionTypeAccessToken:
|
||||
query = p.sqlSelectOAuth2AccessTokenSession
|
||||
case OAuth2SessionTypeRefreshToken:
|
||||
query = p.sqlSelectOAuth2RefreshTokenSession
|
||||
case OAuth2SessionTypePKCEChallenge:
|
||||
query = p.sqlSelectOAuth2PKCERequestSession
|
||||
case OAuth2SessionTypeOpenIDConnect:
|
||||
query = p.sqlSelectOAuth2OpenIDConnectSession
|
||||
default:
|
||||
return nil, fmt.Errorf("error selecting oauth2 session: unknown oauth2 session type '%s'", sessionType)
|
||||
}
|
||||
|
||||
session = &model.OAuth2Session{}
|
||||
|
||||
if err = p.db.GetContext(ctx, session, query, signature); err != nil {
|
||||
return nil, fmt.Errorf("error selecting oauth2 %s session with signature '%s': %w", sessionType, signature, err)
|
||||
}
|
||||
|
||||
if session.Session, err = p.decrypt(session.Session); err != nil {
|
||||
return nil, fmt.Errorf("error decrypting the oauth2 %s session data with signature '%s' for subject '%s' and request id '%s': %w", sessionType, signature, session.Subject, session.RequestID, err)
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// SaveOAuth2BlacklistedJTI saves a OAuth2BlacklistedJTI to the database.
|
||||
func (p *SQLProvider) SaveOAuth2BlacklistedJTI(ctx context.Context, blacklistedJTI model.OAuth2BlacklistedJTI) (err error) {
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlUpsertOAuth2BlacklistedJTI, blacklistedJTI.Signature, blacklistedJTI.ExpiresAt); err != nil {
|
||||
return fmt.Errorf("error inserting oauth2 blacklisted JTI with signature '%s': %w", blacklistedJTI.Signature, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadOAuth2BlacklistedJTI loads a OAuth2BlacklistedJTI from the database.
|
||||
func (p *SQLProvider) LoadOAuth2BlacklistedJTI(ctx context.Context, signature string) (blacklistedJTI *model.OAuth2BlacklistedJTI, err error) {
|
||||
blacklistedJTI = &model.OAuth2BlacklistedJTI{}
|
||||
|
||||
if err = p.db.GetContext(ctx, blacklistedJTI, p.sqlSelectOAuth2BlacklistedJTI, signature); err != nil {
|
||||
return nil, fmt.Errorf("error selecting oauth2 blacklisted JTI with signature '%s': %w", blacklistedJTI.Signature, err)
|
||||
}
|
||||
|
||||
return blacklistedJTI, nil
|
||||
}
|
||||
|
||||
// SavePreferred2FAMethod save the preferred method for 2FA to the database.
|
||||
func (p *SQLProvider) SavePreferred2FAMethod(ctx context.Context, username string, method string) (err error) {
|
||||
if _, err = p.db.ExecContext(ctx, p.sqlUpsertPreferred2FAMethod, username, method); err != nil {
|
||||
|
|
|
@ -26,19 +26,27 @@ func NewPostgreSQLProvider(config *schema.Configuration) (provider *PostgreSQLPr
|
|||
|
||||
// Specific alterations to this provider.
|
||||
// PostgreSQL doesn't have a UPSERT statement but has an ON CONFLICT operation instead.
|
||||
provider.sqlUpsertWebauthnDevice = fmt.Sprintf(queryFmtPostgresUpsertWebauthnDevice, tableWebauthnDevices)
|
||||
provider.sqlUpsertDuoDevice = fmt.Sprintf(queryFmtPostgresUpsertDuoDevice, tableDuoDevices)
|
||||
provider.sqlUpsertTOTPConfig = fmt.Sprintf(queryFmtPostgresUpsertTOTPConfiguration, tableTOTPConfigurations)
|
||||
provider.sqlUpsertPreferred2FAMethod = fmt.Sprintf(queryFmtPostgresUpsertPreferred2FAMethod, tableUserPreferences)
|
||||
provider.sqlUpsertEncryptionValue = fmt.Sprintf(queryFmtPostgresUpsertEncryptionValue, tableEncryption)
|
||||
provider.sqlUpsertWebauthnDevice = fmt.Sprintf(queryFmtUpsertWebauthnDevicePostgreSQL, tableWebauthnDevices)
|
||||
provider.sqlUpsertDuoDevice = fmt.Sprintf(queryFmtUpsertDuoDevicePostgreSQL, tableDuoDevices)
|
||||
provider.sqlUpsertTOTPConfig = fmt.Sprintf(queryFmtUpsertTOTPConfigurationPostgreSQL, tableTOTPConfigurations)
|
||||
provider.sqlUpsertPreferred2FAMethod = fmt.Sprintf(queryFmtUpsertPreferred2FAMethodPostgreSQL, tableUserPreferences)
|
||||
provider.sqlUpsertEncryptionValue = fmt.Sprintf(queryFmtUpsertEncryptionValuePostgreSQL, tableEncryption)
|
||||
provider.sqlUpsertOAuth2BlacklistedJTI = fmt.Sprintf(queryFmtUpsertOAuth2BlacklistedJTIPostgreSQL, tableOAuth2BlacklistedJTI)
|
||||
|
||||
// PostgreSQL requires rebinding of any query that contains a '?' placeholder to use the '$#' notation placeholders.
|
||||
provider.sqlFmtRenameTable = provider.db.Rebind(provider.sqlFmtRenameTable)
|
||||
|
||||
provider.sqlSelectPreferred2FAMethod = provider.db.Rebind(provider.sqlSelectPreferred2FAMethod)
|
||||
provider.sqlSelectUserInfo = provider.db.Rebind(provider.sqlSelectUserInfo)
|
||||
|
||||
provider.sqlInsertUserOpaqueIdentifier = provider.db.Rebind(provider.sqlInsertUserOpaqueIdentifier)
|
||||
provider.sqlSelectUserOpaqueIdentifier = provider.db.Rebind(provider.sqlSelectUserOpaqueIdentifier)
|
||||
provider.sqlSelectUserOpaqueIdentifierBySignature = provider.db.Rebind(provider.sqlSelectUserOpaqueIdentifierBySignature)
|
||||
|
||||
provider.sqlSelectIdentityVerification = provider.db.Rebind(provider.sqlSelectIdentityVerification)
|
||||
provider.sqlInsertIdentityVerification = provider.db.Rebind(provider.sqlInsertIdentityVerification)
|
||||
provider.sqlConsumeIdentityVerification = provider.db.Rebind(provider.sqlConsumeIdentityVerification)
|
||||
|
||||
provider.sqlSelectTOTPConfig = provider.db.Rebind(provider.sqlSelectTOTPConfig)
|
||||
provider.sqlUpdateTOTPConfigRecordSignIn = provider.db.Rebind(provider.sqlUpdateTOTPConfigRecordSignIn)
|
||||
provider.sqlUpdateTOTPConfigRecordSignInByUsername = provider.db.Rebind(provider.sqlUpdateTOTPConfigRecordSignInByUsername)
|
||||
|
@ -46,21 +54,69 @@ func NewPostgreSQLProvider(config *schema.Configuration) (provider *PostgreSQLPr
|
|||
provider.sqlSelectTOTPConfigs = provider.db.Rebind(provider.sqlSelectTOTPConfigs)
|
||||
provider.sqlUpdateTOTPConfigSecret = provider.db.Rebind(provider.sqlUpdateTOTPConfigSecret)
|
||||
provider.sqlUpdateTOTPConfigSecretByUsername = provider.db.Rebind(provider.sqlUpdateTOTPConfigSecretByUsername)
|
||||
|
||||
provider.sqlSelectWebauthnDevices = provider.db.Rebind(provider.sqlSelectWebauthnDevices)
|
||||
provider.sqlSelectWebauthnDevicesByUsername = provider.db.Rebind(provider.sqlSelectWebauthnDevicesByUsername)
|
||||
provider.sqlUpdateWebauthnDevicePublicKey = provider.db.Rebind(provider.sqlUpdateWebauthnDevicePublicKey)
|
||||
provider.sqlUpdateWebauthnDevicePublicKeyByUsername = provider.db.Rebind(provider.sqlUpdateWebauthnDevicePublicKeyByUsername)
|
||||
provider.sqlUpdateWebauthnDeviceRecordSignIn = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceRecordSignIn)
|
||||
provider.sqlUpdateWebauthnDeviceRecordSignInByUsername = provider.db.Rebind(provider.sqlUpdateWebauthnDeviceRecordSignInByUsername)
|
||||
|
||||
provider.sqlSelectDuoDevice = provider.db.Rebind(provider.sqlSelectDuoDevice)
|
||||
provider.sqlDeleteDuoDevice = provider.db.Rebind(provider.sqlDeleteDuoDevice)
|
||||
|
||||
provider.sqlInsertAuthenticationAttempt = provider.db.Rebind(provider.sqlInsertAuthenticationAttempt)
|
||||
provider.sqlSelectAuthenticationAttemptsByUsername = provider.db.Rebind(provider.sqlSelectAuthenticationAttemptsByUsername)
|
||||
|
||||
provider.sqlInsertMigration = provider.db.Rebind(provider.sqlInsertMigration)
|
||||
provider.sqlSelectMigrations = provider.db.Rebind(provider.sqlSelectMigrations)
|
||||
provider.sqlSelectLatestMigration = provider.db.Rebind(provider.sqlSelectLatestMigration)
|
||||
|
||||
provider.sqlSelectEncryptionValue = provider.db.Rebind(provider.sqlSelectEncryptionValue)
|
||||
|
||||
provider.sqlInsertOAuth2ConsentSession = provider.db.Rebind(provider.sqlInsertOAuth2ConsentSession)
|
||||
provider.sqlUpdateOAuth2ConsentSessionResponse = provider.db.Rebind(provider.sqlUpdateOAuth2ConsentSessionResponse)
|
||||
provider.sqlUpdateOAuth2ConsentSessionGranted = provider.db.Rebind(provider.sqlUpdateOAuth2ConsentSessionGranted)
|
||||
provider.sqlSelectOAuth2ConsentSessionByChallengeID = provider.db.Rebind(provider.sqlSelectOAuth2ConsentSessionByChallengeID)
|
||||
provider.sqlSelectOAuth2ConsentSessionsPreConfigured = provider.db.Rebind(provider.sqlSelectOAuth2ConsentSessionsPreConfigured)
|
||||
|
||||
provider.sqlInsertOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlInsertOAuth2AuthorizeCodeSession)
|
||||
provider.sqlRevokeOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlRevokeOAuth2AuthorizeCodeSession)
|
||||
provider.sqlRevokeOAuth2AuthorizeCodeSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2AuthorizeCodeSessionByRequestID)
|
||||
provider.sqlDeactivateOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlDeactivateOAuth2AuthorizeCodeSession)
|
||||
provider.sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2AuthorizeCodeSessionByRequestID)
|
||||
provider.sqlSelectOAuth2AuthorizeCodeSession = provider.db.Rebind(provider.sqlSelectOAuth2AuthorizeCodeSession)
|
||||
|
||||
provider.sqlInsertOAuth2AccessTokenSession = provider.db.Rebind(provider.sqlInsertOAuth2AccessTokenSession)
|
||||
provider.sqlRevokeOAuth2AccessTokenSession = provider.db.Rebind(provider.sqlRevokeOAuth2AccessTokenSession)
|
||||
provider.sqlRevokeOAuth2AccessTokenSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2AccessTokenSessionByRequestID)
|
||||
provider.sqlDeactivateOAuth2AccessTokenSession = provider.db.Rebind(provider.sqlDeactivateOAuth2AccessTokenSession)
|
||||
provider.sqlDeactivateOAuth2AccessTokenSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2AccessTokenSessionByRequestID)
|
||||
provider.sqlSelectOAuth2AccessTokenSession = provider.db.Rebind(provider.sqlSelectOAuth2AccessTokenSession)
|
||||
|
||||
provider.sqlInsertOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlInsertOAuth2RefreshTokenSession)
|
||||
provider.sqlRevokeOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlRevokeOAuth2RefreshTokenSession)
|
||||
provider.sqlRevokeOAuth2RefreshTokenSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2RefreshTokenSessionByRequestID)
|
||||
provider.sqlDeactivateOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlDeactivateOAuth2RefreshTokenSession)
|
||||
provider.sqlDeactivateOAuth2RefreshTokenSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2RefreshTokenSessionByRequestID)
|
||||
provider.sqlSelectOAuth2RefreshTokenSession = provider.db.Rebind(provider.sqlSelectOAuth2RefreshTokenSession)
|
||||
|
||||
provider.sqlInsertOAuth2PKCERequestSession = provider.db.Rebind(provider.sqlInsertOAuth2PKCERequestSession)
|
||||
provider.sqlRevokeOAuth2PKCERequestSession = provider.db.Rebind(provider.sqlRevokeOAuth2PKCERequestSession)
|
||||
provider.sqlRevokeOAuth2PKCERequestSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2PKCERequestSessionByRequestID)
|
||||
provider.sqlDeactivateOAuth2PKCERequestSession = provider.db.Rebind(provider.sqlDeactivateOAuth2PKCERequestSession)
|
||||
provider.sqlDeactivateOAuth2PKCERequestSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2PKCERequestSessionByRequestID)
|
||||
provider.sqlSelectOAuth2PKCERequestSession = provider.db.Rebind(provider.sqlSelectOAuth2PKCERequestSession)
|
||||
|
||||
provider.sqlInsertOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlInsertOAuth2OpenIDConnectSession)
|
||||
provider.sqlRevokeOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlRevokeOAuth2OpenIDConnectSession)
|
||||
provider.sqlRevokeOAuth2OpenIDConnectSessionByRequestID = provider.db.Rebind(provider.sqlRevokeOAuth2OpenIDConnectSessionByRequestID)
|
||||
provider.sqlDeactivateOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlDeactivateOAuth2OpenIDConnectSession)
|
||||
provider.sqlDeactivateOAuth2OpenIDConnectSessionByRequestID = provider.db.Rebind(provider.sqlDeactivateOAuth2OpenIDConnectSessionByRequestID)
|
||||
provider.sqlSelectOAuth2OpenIDConnectSession = provider.db.Rebind(provider.sqlSelectOAuth2OpenIDConnectSession)
|
||||
|
||||
provider.sqlSelectOAuth2BlacklistedJTI = provider.db.Rebind(provider.sqlSelectOAuth2BlacklistedJTI)
|
||||
|
||||
provider.schema = config.Storage.PostgreSQL.Schema
|
||||
|
||||
return provider
|
||||
|
|
|
@ -48,7 +48,7 @@ const (
|
|||
REPLACE INTO %s (username, second_factor_method)
|
||||
VALUES (?, ?);`
|
||||
|
||||
queryFmtPostgresUpsertPreferred2FAMethod = `
|
||||
queryFmtUpsertPreferred2FAMethodPostgreSQL = `
|
||||
INSERT INTO %s (username, second_factor_method)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (username)
|
||||
|
@ -99,7 +99,7 @@ const (
|
|||
REPLACE INTO %s (created_at, last_used_at, username, issuer, algorithm, digits, period, secret)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?);`
|
||||
|
||||
queryFmtPostgresUpsertTOTPConfiguration = `
|
||||
queryFmtUpsertTOTPConfigurationPostgreSQL = `
|
||||
INSERT INTO %s (created_at, last_used_at, username, issuer, algorithm, digits, period, secret)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (username)
|
||||
|
@ -160,7 +160,7 @@ const (
|
|||
REPLACE INTO %s (created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
|
||||
|
||||
queryFmtPostgresUpsertWebauthnDevice = `
|
||||
queryFmtUpsertWebauthnDevicePostgreSQL = `
|
||||
INSERT INTO %s (created_at, last_used_at, rpid, username, description, kid, public_key, attestation_type, transport, aaguid, sign_count, clone_warning)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
ON CONFLICT (username, description)
|
||||
|
@ -172,7 +172,7 @@ const (
|
|||
REPLACE INTO %s (username, device, method)
|
||||
VALUES (?, ?, ?);`
|
||||
|
||||
queryFmtPostgresUpsertDuoDevice = `
|
||||
queryFmtUpsertDuoDevicePostgreSQL = `
|
||||
INSERT INTO %s (username, device, method)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (username)
|
||||
|
@ -214,9 +214,103 @@ const (
|
|||
REPLACE INTO %s (name, value)
|
||||
VALUES (?, ?);`
|
||||
|
||||
queryFmtPostgresUpsertEncryptionValue = `
|
||||
queryFmtUpsertEncryptionValuePostgreSQL = `
|
||||
INSERT INTO %s (name, value)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (name)
|
||||
DO UPDATE SET value = $2;`
|
||||
)
|
||||
|
||||
const (
|
||||
queryFmtSelectOAuth2ConsentSessionByChallengeID = `
|
||||
SELECT id, challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, expires_at,
|
||||
form_data, requested_scopes, granted_scopes, requested_audience, granted_audience
|
||||
FROM %s
|
||||
WHERE challenge_id = ?;`
|
||||
|
||||
queryFmtSelectOAuth2ConsentSessionsPreConfigured = `
|
||||
SELECT id, challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, expires_at,
|
||||
form_data, requested_scopes, granted_scopes, requested_audience, granted_audience
|
||||
FROM %s
|
||||
WHERE client_id = ? AND subject = ? AND
|
||||
authorized = TRUE AND granted = TRUE AND expires_at IS NOT NULL AND expires_at >= CURRENT_TIMESTAMP;`
|
||||
|
||||
queryFmtInsertOAuth2ConsentSession = `
|
||||
INSERT INTO %s (challenge_id, client_id, subject, authorized, granted, requested_at, responded_at, expires_at,
|
||||
form_data, requested_scopes, granted_scopes, requested_audience, granted_audience)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
|
||||
|
||||
queryFmtUpdateOAuth2ConsentSessionResponse = `
|
||||
UPDATE %s
|
||||
SET authorized = ?, responded_at = CURRENT_TIMESTAMP, expires_at = ?, granted_scopes = ?, granted_audience = ?
|
||||
WHERE id = ? AND responded_at IS NULL;`
|
||||
|
||||
queryFmtUpdateOAuth2ConsentSessionGranted = `
|
||||
UPDATE %s
|
||||
SET granted = TRUE
|
||||
WHERE id = ? AND responded_at IS NOT NULL;`
|
||||
|
||||
queryFmtSelectOAuth2Session = `
|
||||
SELECT id, challenge_id, request_id, client_id, signature, subject, requested_at,
|
||||
requested_scopes, granted_scopes, requested_audience, granted_audience,
|
||||
active, revoked, form_data, session_data
|
||||
FROM %s
|
||||
WHERE signature = ? AND revoked = FALSE;`
|
||||
|
||||
queryFmtInsertOAuth2Session = `
|
||||
INSERT INTO %s (challenge_id, request_id, client_id, signature, subject, requested_at,
|
||||
requested_scopes, granted_scopes, requested_audience, granted_audience,
|
||||
active, revoked, form_data, session_data)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`
|
||||
|
||||
queryFmtRevokeOAuth2Session = `
|
||||
UPDATE %s
|
||||
SET revoked = TRUE
|
||||
WHERE signature = ?;`
|
||||
|
||||
queryFmtRevokeOAuth2SessionByRequestID = `
|
||||
UPDATE %s
|
||||
SET revoked = TRUE
|
||||
WHERE request_id = ?;`
|
||||
|
||||
queryFmtDeactivateOAuth2Session = `
|
||||
UPDATE %s
|
||||
SET active = FALSE
|
||||
WHERE signature = ?;`
|
||||
|
||||
queryFmtDeactivateOAuth2SessionByRequestID = `
|
||||
UPDATE %s
|
||||
SET active = FALSE
|
||||
WHERE request_id = ?;"`
|
||||
|
||||
queryFmtSelectOAuth2BlacklistedJTI = `
|
||||
SELECT id, signature, expires_at
|
||||
FROM %s
|
||||
WHERE signature = ?;`
|
||||
|
||||
queryFmtUpsertOAuth2BlacklistedJTI = `
|
||||
REPLACE INTO %s (signature, expires_at)
|
||||
VALUES(?, ?);`
|
||||
|
||||
queryFmtUpsertOAuth2BlacklistedJTIPostgreSQL = `
|
||||
INSERT INTO %s (signature, expires_at)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (signature)
|
||||
DO UPDATE SET expires_at = $2;`
|
||||
)
|
||||
|
||||
const (
|
||||
queryFmtInsertUserOpaqueIdentifier = `
|
||||
INSERT INTO %s (service, sector_id, username, identifier)
|
||||
VALUES(?, ?, ?, ?);`
|
||||
|
||||
queryFmtSelectUserOpaqueIdentifier = `
|
||||
SELECT id, sector_id, username, identifier
|
||||
FROM %s
|
||||
WHERE identifier = ?;`
|
||||
|
||||
queryFmtSelectUserOpaqueIdentifierBySignature = `
|
||||
SELECT id, service, sector_id, username, identifier
|
||||
FROM %s
|
||||
WHERE service = ? AND sector_id = ? AND username = ?;`
|
||||
)
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"github.com/authelia/authelia/v4/internal/model"
|
||||
)
|
||||
|
||||
// ConsentSessionRows holds and assists with retrieving multiple model.OAuth2ConsentSession rows.
|
||||
type ConsentSessionRows struct {
|
||||
rows *sqlx.Rows
|
||||
}
|
||||
|
||||
// Next is the row iterator.
|
||||
func (r *ConsentSessionRows) Next() bool {
|
||||
if r.rows == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return r.rows.Next()
|
||||
}
|
||||
|
||||
// Close the rows.
|
||||
func (r *ConsentSessionRows) Close() (err error) {
|
||||
if r.rows == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return r.rows.Close()
|
||||
}
|
||||
|
||||
// Get returns the *model.OAuth2ConsentSession or scan error.
|
||||
func (r *ConsentSessionRows) Get() (consent *model.OAuth2ConsentSession, err error) {
|
||||
if r.rows == nil {
|
||||
return nil, sql.ErrNoRows
|
||||
}
|
||||
|
||||
consent = &model.OAuth2ConsentSession{}
|
||||
|
||||
if err = r.rows.StructScan(consent); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return consent, nil
|
||||
}
|
|
@ -58,6 +58,7 @@ notifier:
|
|||
|
||||
identity_providers:
|
||||
oidc:
|
||||
enable_client_debug_messages: true
|
||||
hmac_secret: IVPWBkAdJHje3uz7LtFTDU2pFUfh39Xm
|
||||
issuer_private_key: |
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
|
|
|
@ -60,6 +60,7 @@ notifier:
|
|||
|
||||
identity_providers:
|
||||
oidc:
|
||||
enable_client_debug_messages: true
|
||||
hmac_secret: IVPWBkAdJHje3uz7LtFTDU2pFUfh39Xm
|
||||
issuer_private_key: |
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
|
|
|
@ -9,26 +9,26 @@ import (
|
|||
)
|
||||
|
||||
func (rs *RodSession) doChangeMethod(t *testing.T, page *rod.Page, method string) {
|
||||
err := rs.WaitElementLocatedByCSSSelector(t, page, "methods-button").Click("left")
|
||||
err := rs.WaitElementLocatedByID(t, page, "methods-button").Click("left")
|
||||
require.NoError(t, err)
|
||||
rs.WaitElementLocatedByCSSSelector(t, page, "methods-dialog")
|
||||
err = rs.WaitElementLocatedByCSSSelector(t, page, fmt.Sprintf("%s-option", method)).Click("left")
|
||||
rs.WaitElementLocatedByID(t, page, "methods-dialog")
|
||||
err = rs.WaitElementLocatedByID(t, page, fmt.Sprintf("%s-option", method)).Click("left")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func (rs *RodSession) doChangeDevice(t *testing.T, page *rod.Page, deviceID string) {
|
||||
err := rs.WaitElementLocatedByCSSSelector(t, page, "selection-link").Click("left")
|
||||
err := rs.WaitElementLocatedByID(t, page, "selection-link").Click("left")
|
||||
require.NoError(t, err)
|
||||
rs.doSelectDevice(t, page, deviceID)
|
||||
}
|
||||
|
||||
func (rs *RodSession) doSelectDevice(t *testing.T, page *rod.Page, deviceID string) {
|
||||
rs.WaitElementLocatedByCSSSelector(t, page, "device-selection")
|
||||
err := rs.WaitElementLocatedByCSSSelector(t, page, fmt.Sprintf("device-%s", deviceID)).Click("left")
|
||||
rs.WaitElementLocatedByID(t, page, "device-selection")
|
||||
err := rs.WaitElementLocatedByID(t, page, fmt.Sprintf("device-%s", deviceID)).Click("left")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func (rs *RodSession) doClickButton(t *testing.T, page *rod.Page, buttonID string) {
|
||||
err := rs.WaitElementLocatedByCSSSelector(t, page, buttonID).Click("left")
|
||||
err := rs.WaitElementLocatedByID(t, page, buttonID).Click("left")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
|
@ -9,21 +9,21 @@ import (
|
|||
)
|
||||
|
||||
func (rs *RodSession) doFillLoginPageAndClick(t *testing.T, page *rod.Page, username, password string, keepMeLoggedIn bool) {
|
||||
usernameElement := rs.WaitElementLocatedByCSSSelector(t, page, "username-textfield")
|
||||
usernameElement := rs.WaitElementLocatedByID(t, page, "username-textfield")
|
||||
err := usernameElement.Input(username)
|
||||
require.NoError(t, err)
|
||||
|
||||
passwordElement := rs.WaitElementLocatedByCSSSelector(t, page, "password-textfield")
|
||||
passwordElement := rs.WaitElementLocatedByID(t, page, "password-textfield")
|
||||
err = passwordElement.Input(password)
|
||||
require.NoError(t, err)
|
||||
|
||||
if keepMeLoggedIn {
|
||||
keepMeLoggedInElement := rs.WaitElementLocatedByCSSSelector(t, page, "remember-checkbox")
|
||||
keepMeLoggedInElement := rs.WaitElementLocatedByID(t, page, "remember-checkbox")
|
||||
err = keepMeLoggedInElement.Click("left")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
buttonElement := rs.WaitElementLocatedByCSSSelector(t, page, "sign-in-button")
|
||||
buttonElement := rs.WaitElementLocatedByID(t, page, "sign-in-button")
|
||||
err = buttonElement.Click("left")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
|
@ -9,13 +9,13 @@ import (
|
|||
)
|
||||
|
||||
func (rs *RodSession) doInitiatePasswordReset(t *testing.T, page *rod.Page, username string) {
|
||||
err := rs.WaitElementLocatedByCSSSelector(t, page, "reset-password-button").Click("left")
|
||||
err := rs.WaitElementLocatedByID(t, page, "reset-password-button").Click("left")
|
||||
require.NoError(t, err)
|
||||
// Fill in username.
|
||||
err = rs.WaitElementLocatedByCSSSelector(t, page, "username-textfield").Input(username)
|
||||
err = rs.WaitElementLocatedByID(t, page, "username-textfield").Input(username)
|
||||
require.NoError(t, err)
|
||||
// And click on the reset button.
|
||||
err = rs.WaitElementLocatedByCSSSelector(t, page, "reset-button").Click("left")
|
||||
err = rs.WaitElementLocatedByID(t, page, "reset-button").Click("left")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
@ -25,15 +25,15 @@ func (rs *RodSession) doCompletePasswordReset(t *testing.T, page *rod.Page, newP
|
|||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
err := rs.WaitElementLocatedByCSSSelector(t, page, "password1-textfield").Input(newPassword1)
|
||||
err := rs.WaitElementLocatedByID(t, page, "password1-textfield").Input(newPassword1)
|
||||
require.NoError(t, err)
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
err = rs.WaitElementLocatedByCSSSelector(t, page, "password2-textfield").Input(newPassword2)
|
||||
err = rs.WaitElementLocatedByID(t, page, "password2-textfield").Input(newPassword2)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = rs.WaitElementLocatedByCSSSelector(t, page, "reset-button").Click("left")
|
||||
err = rs.WaitElementLocatedByID(t, page, "reset-button").Click("left")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
)
|
||||
|
||||
func (rs *RodSession) doRegisterTOTP(t *testing.T, page *rod.Page) string {
|
||||
err := rs.WaitElementLocatedByCSSSelector(t, page, "register-link").Click("left")
|
||||
err := rs.WaitElementLocatedByID(t, page, "register-link").Click("left")
|
||||
require.NoError(t, err)
|
||||
rs.verifyMailNotificationDisplayed(t, page)
|
||||
link := doGetLinkFromLastMail(t)
|
||||
|
@ -28,7 +28,7 @@ func (rs *RodSession) doRegisterTOTP(t *testing.T, page *rod.Page) string {
|
|||
}
|
||||
|
||||
func (rs *RodSession) doEnterOTP(t *testing.T, page *rod.Page, code string) {
|
||||
inputs := rs.WaitElementsLocatedByCSSSelector(t, page, "otp-input input")
|
||||
inputs := rs.WaitElementsLocatedByID(t, page, "otp-input input")
|
||||
|
||||
for i := 0; i < len(code); i++ {
|
||||
_ = inputs[i].Input(string(code[i]))
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
version: '3'
|
||||
services:
|
||||
oidc-client:
|
||||
image: ghcr.io/authelia/oidc-tester-app:master-89622a8
|
||||
image: ghcr.io/authelia/oidc-tester-app:master-01ff268
|
||||
command: /entrypoint.sh
|
||||
depends_on:
|
||||
- authelia-backend
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
while true;
|
||||
do
|
||||
oidc-tester-app --issuer https://login.example.com:8080 --id oidc-tester-app --secret foobar --scopes openid,profile,email --public-url https://oidc.example.com:8080
|
||||
oidc-tester-app --issuer=https://login.example.com:8080 --id=oidc-tester-app --secret=foobar --scopes=openid,profile,email,groups --public-url=https://oidc.example.com:8080
|
||||
sleep 5
|
||||
done
|
|
@ -58,11 +58,11 @@ func (s *AvailableMethodsScenario) TestShouldCheckAvailableMethods() {
|
|||
|
||||
s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "")
|
||||
|
||||
methodsButton := s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "methods-button")
|
||||
methodsButton := s.WaitElementLocatedByID(s.T(), s.Context(ctx), "methods-button")
|
||||
err := methodsButton.Click("left")
|
||||
s.Assert().NoError(err)
|
||||
|
||||
methodsDialog := s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "methods-dialog")
|
||||
methodsDialog := s.WaitElementLocatedByID(s.T(), s.Context(ctx), "methods-dialog")
|
||||
options, err := methodsDialog.Elements(".method-option")
|
||||
s.Assert().NoError(err)
|
||||
s.Assert().Len(options, len(s.methods))
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -89,11 +90,50 @@ func (s *OIDCScenario) TestShouldAuthorizeAccessToOIDCApp() {
|
|||
assert.NoError(s.T(), err)
|
||||
|
||||
s.verifyIsConsentPage(s.T(), s.Context(ctx))
|
||||
err = s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "accept-button").Click("left")
|
||||
err = s.WaitElementLocatedByID(s.T(), s.Context(ctx), "accept-button").Click("left")
|
||||
assert.NoError(s.T(), err)
|
||||
|
||||
// Verify that the app is showing the info related to the user stored in the JWT token.
|
||||
s.waitBodyContains(s.T(), s.Context(ctx), "Logged in as john!")
|
||||
|
||||
rUUID := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
|
||||
rInteger := regexp.MustCompile(`^\d+$`)
|
||||
rBoolean := regexp.MustCompile(`^(true|false)$`)
|
||||
rBase64 := regexp.MustCompile(`^[-_A-Za-z0-9+\\/]+([=]{0,3})$`)
|
||||
|
||||
testCases := []struct {
|
||||
desc, elementID, elementText string
|
||||
pattern *regexp.Regexp
|
||||
}{
|
||||
{"welcome", "welcome", "Logged in as john!", nil},
|
||||
{"at_hash", "claim-at_hash", "", rBase64},
|
||||
{"jti", "claim-jti", "", rUUID},
|
||||
{"iat", "claim-iat", "", rInteger},
|
||||
{"nbf", "claim-nbf", "", rInteger},
|
||||
{"rat", "claim-rat", "", rInteger},
|
||||
{"expires", "claim-exp", "", rInteger},
|
||||
{"amr", "claim-amr", "pwd, otp, mfa", nil},
|
||||
{"acr", "claim-acr", "", nil},
|
||||
{"issuer", "claim-iss", "https://login.example.com:8080", nil},
|
||||
{"name", "claim-name", "John Doe", nil},
|
||||
{"preferred_username", "claim-preferred_username", "john", nil},
|
||||
{"groups", "claim-groups", "admins, dev", nil},
|
||||
{"email", "claim-email", "john.doe@authelia.com", nil},
|
||||
{"email_verified", "claim-email_verified", "", rBoolean},
|
||||
}
|
||||
|
||||
var text string
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.T().Run(fmt.Sprintf("check_claims/%s", tc.desc), func(t *testing.T) {
|
||||
text, err = s.WaitElementLocatedByID(t, s.Context(ctx), tc.elementID).Text()
|
||||
assert.NoError(t, err)
|
||||
if tc.pattern == nil {
|
||||
assert.Equal(t, tc.elementText, text)
|
||||
} else {
|
||||
assert.Regexp(t, tc.pattern, text)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OIDCScenario) TestShouldDenyConsent() {
|
||||
|
@ -117,10 +157,17 @@ func (s *OIDCScenario) TestShouldDenyConsent() {
|
|||
|
||||
s.verifyIsConsentPage(s.T(), s.Context(ctx))
|
||||
|
||||
err = s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "deny-button").Click("left")
|
||||
err = s.WaitElementLocatedByID(s.T(), s.Context(ctx), "deny-button").Click("left")
|
||||
assert.NoError(s.T(), err)
|
||||
|
||||
s.verifyIsOIDC(s.T(), s.Context(ctx), "oauth2:", "https://oidc.example.com:8080/oauth2/callback?error=access_denied&error_description=User%20has%20rejected%20the%20scopes")
|
||||
s.verifyIsOIDC(s.T(), s.Context(ctx), "access_denied", "https://oidc.example.com:8080/error?error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request.+Make+sure+that+the+request+you+are+making+is+valid.+Maybe+the+credential+or+request+parameters+you+are+using+are+limited+in+scope+or+otherwise+restricted.&state=random-string-here")
|
||||
|
||||
errorDescription := "The resource owner or authorization server denied the request. Make sure that the request " +
|
||||
"you are making is valid. Maybe the credential or request parameters you are using are limited in scope or " +
|
||||
"otherwise restricted."
|
||||
|
||||
s.verifyIsOIDCErrorPage(s.T(), s.Context(ctx), "access_denied", errorDescription, "",
|
||||
"random-string-here")
|
||||
}
|
||||
|
||||
func TestRunOIDCScenario(t *testing.T) {
|
||||
|
|
|
@ -60,16 +60,16 @@ func (s *RegulationScenario) TestShouldBanUserAfterTooManyAttempt() {
|
|||
s.verifyNotificationDisplayed(s.T(), s.Context(ctx), "Incorrect username or password.")
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
err := s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "password-textfield").Input("bad-password")
|
||||
err := s.WaitElementLocatedByID(s.T(), s.Context(ctx), "password-textfield").Input("bad-password")
|
||||
require.NoError(s.T(), err)
|
||||
err = s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "sign-in-button").Click("left")
|
||||
err = s.WaitElementLocatedByID(s.T(), s.Context(ctx), "sign-in-button").Click("left")
|
||||
require.NoError(s.T(), err)
|
||||
}
|
||||
|
||||
// Enter the correct password and test the regulation lock out.
|
||||
err := s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "password-textfield").Input("password")
|
||||
err := s.WaitElementLocatedByID(s.T(), s.Context(ctx), "password-textfield").Input("password")
|
||||
require.NoError(s.T(), err)
|
||||
err = s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "sign-in-button").Click("left")
|
||||
err = s.WaitElementLocatedByID(s.T(), s.Context(ctx), "sign-in-button").Click("left")
|
||||
require.NoError(s.T(), err)
|
||||
s.verifyNotificationDisplayed(s.T(), s.Context(ctx), "Incorrect username or password.")
|
||||
|
||||
|
@ -77,9 +77,9 @@ func (s *RegulationScenario) TestShouldBanUserAfterTooManyAttempt() {
|
|||
time.Sleep(10 * time.Second)
|
||||
|
||||
// Enter the correct password and test a successful login.
|
||||
err = s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "password-textfield").Input("password")
|
||||
err = s.WaitElementLocatedByID(s.T(), s.Context(ctx), "password-textfield").Input("password")
|
||||
require.NoError(s.T(), err)
|
||||
err = s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "sign-in-button").Click("left")
|
||||
err = s.WaitElementLocatedByID(s.T(), s.Context(ctx), "sign-in-button").Click("left")
|
||||
require.NoError(s.T(), err)
|
||||
s.verifyIsSecondFactorPage(s.T(), s.Context(ctx))
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ func (s *UserPreferencesScenario) TestShouldRememberLastUsed2FAMethod() {
|
|||
|
||||
// Then switch to push notification method.
|
||||
s.doChangeMethod(s.T(), s.Context(ctx), "push-notification")
|
||||
s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "push-notification-method")
|
||||
s.WaitElementLocatedByID(s.T(), s.Context(ctx), "push-notification-method")
|
||||
|
||||
// Switch context to clean up state in portal.
|
||||
s.doVisit(s.T(), s.Context(ctx), HomeBaseURL)
|
||||
|
@ -70,7 +70,7 @@ func (s *UserPreferencesScenario) TestShouldRememberLastUsed2FAMethod() {
|
|||
s.doVisit(s.T(), s.Context(ctx), GetLoginBaseURL())
|
||||
s.verifyIsSecondFactorPage(s.T(), s.Context(ctx))
|
||||
// And check the latest method is still used.
|
||||
s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "push-notification-method")
|
||||
s.WaitElementLocatedByID(s.T(), s.Context(ctx), "push-notification-method")
|
||||
// Meaning the authentication is successful.
|
||||
s.verifyIsHome(s.T(), s.Context(ctx))
|
||||
|
||||
|
@ -78,7 +78,7 @@ func (s *UserPreferencesScenario) TestShouldRememberLastUsed2FAMethod() {
|
|||
s.doLogout(s.T(), s.Context(ctx))
|
||||
s.doLoginOneFactor(s.T(), s.Context(ctx), "harry", "password", false, "")
|
||||
s.verifyIsSecondFactorPage(s.T(), s.Context(ctx))
|
||||
s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "one-time-password-method")
|
||||
s.WaitElementLocatedByID(s.T(), s.Context(ctx), "one-time-password-method")
|
||||
|
||||
s.doLogout(s.T(), s.Context(ctx))
|
||||
s.verifyIsFirstFactorPage(s.T(), s.Context(ctx))
|
||||
|
@ -86,7 +86,7 @@ func (s *UserPreferencesScenario) TestShouldRememberLastUsed2FAMethod() {
|
|||
// Then log back as previous user and verify the push notification is still the default method.
|
||||
s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "")
|
||||
s.verifyIsSecondFactorPage(s.T(), s.Context(ctx))
|
||||
s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "push-notification-method")
|
||||
s.WaitElementLocatedByID(s.T(), s.Context(ctx), "push-notification-method")
|
||||
s.verifyIsHome(s.T(), s.Context(ctx))
|
||||
|
||||
s.doLogout(s.T(), s.Context(ctx))
|
||||
|
@ -94,7 +94,7 @@ func (s *UserPreferencesScenario) TestShouldRememberLastUsed2FAMethod() {
|
|||
|
||||
// Eventually restore the default method.
|
||||
s.doChangeMethod(s.T(), s.Context(ctx), "one-time-password")
|
||||
s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "one-time-password-method")
|
||||
s.WaitElementLocatedByID(s.T(), s.Context(ctx), "one-time-password-method")
|
||||
}
|
||||
|
||||
func TestUserPreferencesScenario(t *testing.T) {
|
||||
|
|
|
@ -110,7 +110,7 @@ func (s *DuoPushWebDriverSuite) TestShouldAskUserToRegister() {
|
|||
s.WaitElementLocatedByClassName(s.T(), s.Context(ctx), "state-not-registered")
|
||||
s.verifyNotificationDisplayed(s.T(), s.Context(ctx), "No compatible device found")
|
||||
enrollPage := s.Page.MustWaitOpen()
|
||||
s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "register-link").MustClick()
|
||||
s.WaitElementLocatedByID(s.T(), s.Context(ctx), "register-link").MustClick()
|
||||
s.Page = enrollPage()
|
||||
|
||||
assert.Contains(s.T(), s.WaitElementLocatedByClassName(s.T(), s.Context(ctx), "description").MustText(), "This enrollment code has expired. Contact your administrator to get a new enrollment code.")
|
||||
|
@ -142,7 +142,7 @@ func (s *DuoPushWebDriverSuite) TestShouldAutoSelectDevice() {
|
|||
s.doLogout(s.T(), s.Context(ctx))
|
||||
s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "")
|
||||
// And check the latest method and device is still used.
|
||||
s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "push-notification-method")
|
||||
s.WaitElementLocatedByID(s.T(), s.Context(ctx), "push-notification-method")
|
||||
// Meaning the authentication is successful.
|
||||
s.verifyIsHome(s.T(), s.Context(ctx))
|
||||
}
|
||||
|
@ -176,7 +176,7 @@ func (s *DuoPushWebDriverSuite) TestShouldSelectDevice() {
|
|||
// Switch Method where Device Selection should open automatically.
|
||||
s.doChangeMethod(s.T(), s.Context(ctx), "push-notification")
|
||||
// Check for available Device 1.
|
||||
s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "device-12345ABCDEFGHIJ67890")
|
||||
s.WaitElementLocatedByID(s.T(), s.Context(ctx), "device-12345ABCDEFGHIJ67890")
|
||||
// Test Back button.
|
||||
s.doClickButton(s.T(), s.Context(ctx), "device-selection-back")
|
||||
// then select Device 2 for further use and be redirected.
|
||||
|
@ -187,7 +187,7 @@ func (s *DuoPushWebDriverSuite) TestShouldSelectDevice() {
|
|||
s.doLogout(s.T(), s.Context(ctx))
|
||||
s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "")
|
||||
// And check the latest method and device is still used.
|
||||
s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "push-notification-method")
|
||||
s.WaitElementLocatedByID(s.T(), s.Context(ctx), "push-notification-method")
|
||||
// Meaning the authentication is successful.
|
||||
s.verifyIsHome(s.T(), s.Context(ctx))
|
||||
}
|
||||
|
@ -238,7 +238,7 @@ func (s *DuoPushWebDriverSuite) TestShouldSelectNewDeviceAfterSavedDeviceMethodI
|
|||
|
||||
s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "")
|
||||
s.doChangeMethod(s.T(), s.Context(ctx), "push-notification")
|
||||
s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "device-selection")
|
||||
s.WaitElementLocatedByID(s.T(), s.Context(ctx), "device-selection")
|
||||
s.doSelectDevice(s.T(), s.Context(ctx), "12345ABCDEFGHIJ67890")
|
||||
s.verifyIsHome(s.T(), s.Context(ctx))
|
||||
}
|
||||
|
@ -303,7 +303,7 @@ func (s *DuoPushWebDriverSuite) TestShouldFailSelectionBecauseOfSelectionDenied(
|
|||
|
||||
s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, "")
|
||||
s.doChangeMethod(s.T(), s.Context(ctx), "push-notification")
|
||||
err := s.WaitElementLocatedByCSSSelector(s.T(), s.Context(ctx), "selection-link").Click("left")
|
||||
err := s.WaitElementLocatedByID(s.T(), s.Context(ctx), "selection-link").Click("left")
|
||||
require.NoError(s.T(), err)
|
||||
s.verifyNotificationDisplayed(s.T(), s.Context(ctx), "Device selection was denied by Duo policy")
|
||||
}
|
||||
|
|
|
@ -7,5 +7,5 @@ import (
|
|||
)
|
||||
|
||||
func (rs *RodSession) verifyIsAuthenticatedPage(t *testing.T, page *rod.Page) {
|
||||
rs.WaitElementLocatedByCSSSelector(t, page, "authenticated-stage")
|
||||
rs.WaitElementLocatedByID(t, page, "authenticated-stage")
|
||||
}
|
||||
|
|
|
@ -7,5 +7,5 @@ import (
|
|||
)
|
||||
|
||||
func (rs *RodSession) verifyIsConsentPage(t *testing.T, page *rod.Page) {
|
||||
rs.WaitElementLocatedByCSSSelector(t, page, "consent-stage")
|
||||
rs.WaitElementLocatedByID(t, page, "consent-stage")
|
||||
}
|
||||
|
|
|
@ -7,5 +7,5 @@ import (
|
|||
)
|
||||
|
||||
func (rs *RodSession) verifyIsFirstFactorPage(t *testing.T, page *rod.Page) {
|
||||
rs.WaitElementLocatedByCSSSelector(t, page, "first-factor-stage")
|
||||
rs.WaitElementLocatedByID(t, page, "first-factor-stage")
|
||||
}
|
||||
|
|
|
@ -4,9 +4,33 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func (rs *RodSession) verifyIsOIDC(t *testing.T, page *rod.Page, pattern, url string) {
|
||||
page.MustElementR("body", pattern)
|
||||
rs.verifyURLIs(t, page, url)
|
||||
}
|
||||
|
||||
func (rs *RodSession) verifyIsOIDCErrorPage(t *testing.T, page *rod.Page, errorCode, errorDescription, errorURI, state string) {
|
||||
testCases := []struct {
|
||||
ElementID, ElementText string
|
||||
}{
|
||||
{"error", errorCode},
|
||||
{"error_description", errorDescription},
|
||||
{"error_uri", errorURI},
|
||||
{"state", state},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.ElementID, func(t *testing.T) {
|
||||
if tc.ElementText == "" {
|
||||
t.Skip("Test Skipped as the element is not expected.")
|
||||
}
|
||||
|
||||
text, err := rs.WaitElementLocatedByID(t, page, tc.ElementID).Text()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.ElementText, text)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,5 +7,5 @@ import (
|
|||
)
|
||||
|
||||
func (rs *RodSession) verifyIsSecondFactorPage(t *testing.T, page *rod.Page) {
|
||||
rs.WaitElementLocatedByCSSSelector(t, page, "second-factor-stage")
|
||||
rs.WaitElementLocatedByID(t, page, "second-factor-stage")
|
||||
}
|
||||
|
|
|
@ -7,5 +7,5 @@ import (
|
|||
)
|
||||
|
||||
func (rs *RodSession) verifySecretAuthorized(t *testing.T, page *rod.Page) {
|
||||
rs.WaitElementLocatedByCSSSelector(t, page, "secret")
|
||||
rs.WaitElementLocatedByID(t, page, "secret")
|
||||
}
|
||||
|
|
|
@ -82,8 +82,8 @@ func (rs *RodSession) WaitElementLocatedByClassName(t *testing.T, page *rod.Page
|
|||
return e
|
||||
}
|
||||
|
||||
// WaitElementLocatedByCSSSelector wait an element is located by class name.
|
||||
func (rs *RodSession) WaitElementLocatedByCSSSelector(t *testing.T, page *rod.Page, cssSelector string) *rod.Element {
|
||||
// WaitElementLocatedByID waits for an element located by an id.
|
||||
func (rs *RodSession) WaitElementLocatedByID(t *testing.T, page *rod.Page, cssSelector string) *rod.Element {
|
||||
e, err := page.Element("#" + cssSelector)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, e)
|
||||
|
@ -91,8 +91,8 @@ func (rs *RodSession) WaitElementLocatedByCSSSelector(t *testing.T, page *rod.Pa
|
|||
return e
|
||||
}
|
||||
|
||||
// WaitElementsLocatedByCSSSelector wait an element is located by CSS selector.
|
||||
func (rs *RodSession) WaitElementsLocatedByCSSSelector(t *testing.T, page *rod.Page, cssSelector string) rod.Elements {
|
||||
// WaitElementsLocatedByID waits for an elements located by an id.
|
||||
func (rs *RodSession) WaitElementsLocatedByID(t *testing.T, page *rod.Page, cssSelector string) rod.Elements {
|
||||
e, err := page.Elements("#" + cssSelector)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, e)
|
||||
|
|
|
@ -241,6 +241,39 @@ func StringHTMLEscape(input string) (output string) {
|
|||
return htmlEscaper.Replace(input)
|
||||
}
|
||||
|
||||
// StringJoinDelimitedEscaped joins a string with a specified rune delimiter after escaping any instance of that string
|
||||
// in the string slice. Used with StringSplitDelimitedEscaped.
|
||||
func StringJoinDelimitedEscaped(value []string, delimiter rune) string {
|
||||
escaped := make([]string, len(value))
|
||||
for k, v := range value {
|
||||
escaped[k] = strings.ReplaceAll(v, string(delimiter), "\\"+string(delimiter))
|
||||
}
|
||||
|
||||
return strings.Join(escaped, string(delimiter))
|
||||
}
|
||||
|
||||
// StringSplitDelimitedEscaped splits a string with a specified rune delimiter after unescaping any instance of that
|
||||
// string in the string slice that has been escaped. Used with StringJoinDelimitedEscaped.
|
||||
func StringSplitDelimitedEscaped(value string, delimiter rune) (out []string) {
|
||||
var escape bool
|
||||
|
||||
split := strings.FieldsFunc(value, func(r rune) bool {
|
||||
if r == '\\' {
|
||||
escape = !escape
|
||||
} else if escape && r != delimiter {
|
||||
escape = false
|
||||
}
|
||||
|
||||
return !escape && r == delimiter
|
||||
})
|
||||
|
||||
for k, v := range split {
|
||||
split[k] = strings.ReplaceAll(v, "\\"+string(delimiter), string(delimiter))
|
||||
}
|
||||
|
||||
return split
|
||||
}
|
||||
|
||||
// JoinAndCanonicalizeHeaders join header strings by a given sep.
|
||||
func JoinAndCanonicalizeHeaders(sep []byte, headers ...string) (joined []byte) {
|
||||
for i, header := range headers {
|
||||
|
|
|
@ -8,6 +8,51 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStringSplitDelimitedEscaped(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc, have string
|
||||
delimiter rune
|
||||
want []string
|
||||
}{
|
||||
{desc: "ShouldSplitNormalString", have: "abc,123,456", delimiter: ',', want: []string{"abc", "123", "456"}},
|
||||
{desc: "ShouldSplitEscapedString", have: "a\\,bc,123,456", delimiter: ',', want: []string{"a,bc", "123", "456"}},
|
||||
{desc: "ShouldSplitEscapedStringPipe", have: "a\\|bc|123|456", delimiter: '|', want: []string{"a|bc", "123", "456"}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
actual := StringSplitDelimitedEscaped(tc.have, tc.delimiter)
|
||||
|
||||
assert.Equal(t, tc.want, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringJoinDelimitedEscaped(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc, want string
|
||||
delimiter rune
|
||||
have []string
|
||||
}{
|
||||
{desc: "ShouldJoinNormalStringSlice", have: []string{"abc", "123", "456"}, delimiter: ',', want: "abc,123,456"},
|
||||
{desc: "ShouldJoinEscapeNeededStringSlice", have: []string{"abc", "1,23", "456"}, delimiter: ',', want: "abc,1\\,23,456"},
|
||||
{desc: "ShouldJoinEscapeNeededStringSlicePipe", have: []string{"abc", "1|23", "456"}, delimiter: '|', want: "abc|1\\|23|456"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
actual := StringJoinDelimitedEscaped(tc.have, tc.delimiter)
|
||||
|
||||
assert.Equal(t, tc.want, actual)
|
||||
|
||||
// Ensure splitting again also works fine.
|
||||
split := StringSplitDelimitedEscaped(actual, tc.delimiter)
|
||||
|
||||
assert.Equal(t, tc.have, split)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldNotGenerateSameRandomString(t *testing.T) {
|
||||
randomStringOne := RandomString(10, AlphaNumericCharacters, false)
|
||||
randomStringTwo := RandomString(10, AlphaNumericCharacters, false)
|
||||
|
|
Loading…
Reference in New Issue