Skip to content

Commit

Permalink
feat(oidc): oauth2 discovery support (#2925)
Browse files Browse the repository at this point in the history
* feat(oidc): oauth2 discovery and endpoint rename

This implements the oauth2 authorization server discovery document, adds tests to the discovery documents, implements an efficiency upgrade to these docs, and renames some endpoints to be uniform.
  • Loading branch information
james-d-elliott committed Mar 4, 2022
1 parent b6072e7 commit c9d86a9
Show file tree
Hide file tree
Showing 8 changed files with 776 additions and 125 deletions.
19 changes: 10 additions & 9 deletions docs/configuration/identity-providers/oidc.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,15 +469,16 @@ particularly those that don't use [discovery](https://openid.net/specs/openid-co
appended to the end of the primary URL used to access Authelia. For example in the Discovery example provided you access
Authelia via https://auth.example.com, the discovery URL is https://auth.example.com/.well-known/openid-configuration.

| Endpoint | Path |
|:-------------:|:--------------------------------:|
| Discovery | .well-known/openid-configuration |
| JWKS | api/oidc/jwks |
| Authorization | api/oidc/authorize |
| Token | api/oidc/token |
| Introspection | api/oidc/introspect |
| Revocation | api/oidc/revoke |
| Userinfo | api/oidc/userinfo |
| Endpoint | Path |
|:-------------:|:---------------------------------------------:|
| Discovery | [root]/.well-known/openid-configuration |
| Metadata | [root]/.well-known/oauth-authorization-server |
| JWKS | [root]/api/oidc/jwks |
| Authorization | [root]/api/oidc/authorization |
| Token | [root]/api/oidc/token |
| Introspection | [root]/api/oidc/introspection |
| Revocation | [root]/api/oidc/revocation |
| Userinfo | [root]/api/oidc/userinfo |

[OpenID Connect]: https://openid.net/connect/
[token lifespan]: https://docs.apigee.com/api-platform/antipatterns/oauth-long-expiration
11 changes: 3 additions & 8 deletions internal/handlers/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,9 @@ const (

// OIDC constants.
const (
pathOpenIDConnectWellKnown = "/.well-known/openid-configuration"

pathOpenIDConnectJWKs = "/api/oidc/jwks"
pathOpenIDConnectAuthorization = "/api/oidc/authorize"
pathOpenIDConnectToken = "/api/oidc/token" //nolint:gosec // This is not a hard coded credential, it's a path.
pathOpenIDConnectIntrospection = "/api/oidc/introspect"
pathOpenIDConnectRevocation = "/api/oidc/revoke"
pathOpenIDConnectUserinfo = "/api/oidc/userinfo"
pathLegacyOpenIDConnectAuthorization = "/api/oidc/authorize"
pathLegacyOpenIDConnectIntrospection = "/api/oidc/introspect"
pathLegacyOpenIDConnectRevocation = "/api/oidc/revoke"

// Note: If you change this const you must also do so in the frontend at web/src/services/Api.ts.
pathOpenIDConnectConsent = "/api/oidc/consent"
Expand Down
84 changes: 19 additions & 65 deletions internal/handlers/handler_oidc_wellknown.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ package handlers

import (
"encoding/json"
"fmt"

"github.com/valyala/fasthttp"

"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/oidc"
)

func oidcWellKnown(ctx *middlewares.AutheliaCtx) {
func wellKnownOpenIDConnectConfigurationGET(ctx *middlewares.AutheliaCtx) {
issuer, err := ctx.ExternalRootURL()
if err != nil {
ctx.Logger.Errorf("Error occurred determining OpenID Connect issuer details: %+v", err)
Expand All @@ -19,77 +17,33 @@ func oidcWellKnown(ctx *middlewares.AutheliaCtx) {
return
}

wellKnown := oidc.WellKnownConfiguration{
Issuer: issuer,
JWKSURI: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectJWKs),
wellKnown := ctx.Providers.OpenIDConnect.GetOpenIDConnectWellKnownConfiguration(issuer)

AuthorizationEndpoint: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectAuthorization),
TokenEndpoint: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectToken),
RevocationEndpoint: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectRevocation),
UserinfoEndpoint: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectUserinfo),
IntrospectionEndpoint: fmt.Sprintf("%s%s", issuer, pathOpenIDConnectIntrospection),

Algorithms: []string{"RS256"},
UserinfoAlgorithms: []string{"none", "RS256"},
ctx.SetContentType("application/json")

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{
"offline_access",
oidc.ScopeOpenID,
oidc.ScopeProfile,
oidc.ScopeGroups,
oidc.ScopeEmail,
},
ClaimsSupported: []string{
"aud",
"exp",
"iat",
"iss",
"jti",
"rat",
"sub",
"auth_time",
"nonce",
oidc.ClaimEmail,
oidc.ClaimEmailVerified,
oidc.ClaimEmailAlts,
oidc.ClaimGroups,
oidc.ClaimPreferredUsername,
oidc.ClaimDisplayName,
},
CodeChallengeMethodsSupported: []string{"S256"},
if err = json.NewEncoder(ctx).Encode(wellKnown); err != nil {
ctx.Logger.Errorf("Error occurred in JSON encode: %+v", err)
// TODO: Determine if this is the appropriate error code here.
ctx.Response.SetStatusCode(fasthttp.StatusInternalServerError)

RequestURIParameterSupported: false,
BackChannelLogoutSupported: false,
FrontChannelLogoutSupported: false,
BackChannelLogoutSessionSupported: false,
FrontChannelLogoutSessionSupported: false,
return
}
}

if ctx.Configuration.IdentityProviders.OIDC.EnablePKCEPlainChallenge {
wellKnown.CodeChallengeMethodsSupported = append(wellKnown.CodeChallengeMethodsSupported, "plain")
func wellKnownOAuthAuthorizationServerGET(ctx *middlewares.AutheliaCtx) {
issuer, err := ctx.ExternalRootURL()
if err != nil {
ctx.Logger.Errorf("Error occurred determining OpenID Connect issuer details: %+v", err)
ctx.Response.SetStatusCode(fasthttp.StatusBadRequest)

return
}

wellKnown := ctx.Providers.OpenIDConnect.GetOAuth2WellKnownConfiguration(issuer)

ctx.SetContentType("application/json")

if err := json.NewEncoder(ctx).Encode(wellKnown); err != nil {
if err = json.NewEncoder(ctx).Encode(wellKnown); err != nil {
ctx.Logger.Errorf("Error occurred in JSON encode: %+v", err)
// TODO: Determine if this is the appropriate error code here.
ctx.Response.SetStatusCode(fasthttp.StatusInternalServerError)
Expand Down
23 changes: 14 additions & 9 deletions internal/handlers/oidc_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,34 @@ import (
"github.com/fasthttp/router"

"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/oidc"
)

// RegisterOIDC registers the handlers with the fasthttp *router.Router. TODO: Add paths for UserInfo, Flush, Logout.
// RegisterOIDC registers the handlers with the fasthttp *router.Router. TODO: Add paths for Flush, Logout.
func RegisterOIDC(router *router.Router, middleware middlewares.RequestHandlerBridge) {
// TODO: Add OPTIONS handler.
router.GET(pathOpenIDConnectWellKnown, middleware(oidcWellKnown))
router.GET(oidc.WellKnownOpenIDConfigurationPath, middleware(wellKnownOpenIDConnectConfigurationGET))
router.GET(oidc.WellKnownOAuthAuthorizationServerPath, middleware(wellKnownOAuthAuthorizationServerGET))

router.GET(pathOpenIDConnectConsent, middleware(oidcConsent))

router.POST(pathOpenIDConnectConsent, middleware(oidcConsentPOST))

router.GET(pathOpenIDConnectJWKs, middleware(oidcJWKs))
router.GET(oidc.JWKsPath, middleware(oidcJWKs))

router.GET(pathOpenIDConnectAuthorization, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcAuthorization)))
router.GET(oidc.AuthorizationPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcAuthorization)))
router.GET(pathLegacyOpenIDConnectAuthorization, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcAuthorization)))

// TODO: Add OPTIONS handler.
router.POST(pathOpenIDConnectToken, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcToken)))
router.POST(oidc.TokenPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcToken)))

router.POST(pathOpenIDConnectIntrospection, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcIntrospection)))
router.POST(oidc.IntrospectionPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcIntrospection)))
router.GET(pathLegacyOpenIDConnectIntrospection, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcIntrospection)))

router.GET(pathOpenIDConnectUserinfo, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcUserinfo)))
router.POST(pathOpenIDConnectUserinfo, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcUserinfo)))
router.GET(oidc.UserinfoPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcUserinfo)))
router.POST(oidc.UserinfoPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcUserinfo)))

// TODO: Add OPTIONS handler.
router.POST(pathOpenIDConnectRevocation, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcRevocation)))
router.POST(oidc.RevocationPath, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcRevocation)))
router.POST(pathLegacyOpenIDConnectRevocation, middleware(middlewares.NewHTTPToAutheliaHandlerAdaptor(oidcRevocation)))
}
22 changes: 18 additions & 4 deletions internal/oidc/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package oidc

// Scope strings.
const (
ScopeOpenID = "openid"
ScopeProfile = "profile"
ScopeEmail = "email"
ScopeGroups = "groups"
ScopeOfflineAccess = "offline_access"
ScopeOpenID = "openid"
ScopeProfile = "profile"
ScopeEmail = "email"
ScopeGroups = "groups"
)

// Claim strings.
Expand All @@ -17,3 +18,16 @@ const (
ClaimEmailVerified = "email_verified"
ClaimEmailAlts = "alt_emails"
)

// Paths.
const (
WellKnownOpenIDConfigurationPath = "/.well-known/openid-configuration"
WellKnownOAuthAuthorizationServerPath = "/.well-known/oauth-authorization-server"

JWKsPath = "/api/oidc/jwks"
AuthorizationPath = "/api/oidc/authorization"
TokenPath = "/api/oidc/token" //nolint:gosec // This is not a hard coded credential, it's a path.
IntrospectionPath = "/api/oidc/introspection"
RevocationPath = "/api/oidc/revocation"
UserinfoPath = "/api/oidc/userinfo"
)
112 changes: 112 additions & 0 deletions internal/oidc/provider.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package oidc

import (
"fmt"
"net/http"

"github.com/ory/fosite/compose"
Expand Down Expand Up @@ -88,6 +89,75 @@ 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.herodot = herodot.NewJSONWriter(nil)

return provider, nil
Expand All @@ -107,3 +177,45 @@ func (p OpenIDConnectProvider) WriteError(w http.ResponseWriter, r *http.Request
func (p OpenIDConnectProvider) WriteErrorCode(w http.ResponseWriter, r *http.Request, code int, err error, opts ...herodot.Option) {
p.herodot.WriteErrorCode(w, r, code, err, opts...)
}

// GetOAuth2WellKnownConfiguration returns the discovery document for the OAuth Configuration.
func (p OpenIDConnectProvider) GetOAuth2WellKnownConfiguration(issuer string) OAuth2WellKnownConfiguration {
options := OAuth2WellKnownConfiguration{
CommonDiscoveryOptions: p.discovery.CommonDiscoveryOptions,
OAuth2DiscoveryOptions: p.discovery.OAuth2DiscoveryOptions,
}

options.Issuer = issuer
options.JWKSURI = fmt.Sprintf("%s%s", issuer, JWKsPath)

options.IntrospectionEndpoint = fmt.Sprintf("%s%s", issuer, IntrospectionPath)
options.TokenEndpoint = fmt.Sprintf("%s%s", issuer, TokenPath)

options.AuthorizationEndpoint = fmt.Sprintf("%s%s", issuer, AuthorizationPath)
options.RevocationEndpoint = fmt.Sprintf("%s%s", issuer, RevocationPath)

return options
}

// GetOpenIDConnectWellKnownConfiguration returns the discovery document for the OpenID Configuration.
func (p OpenIDConnectProvider) GetOpenIDConnectWellKnownConfiguration(issuer string) OpenIDConnectWellKnownConfiguration {
options := OpenIDConnectWellKnownConfiguration{
CommonDiscoveryOptions: p.discovery.CommonDiscoveryOptions,
OAuth2DiscoveryOptions: p.discovery.OAuth2DiscoveryOptions,
OpenIDConnectDiscoveryOptions: p.discovery.OpenIDConnectDiscoveryOptions,
OpenIDConnectFrontChannelLogoutDiscoveryOptions: p.discovery.OpenIDConnectFrontChannelLogoutDiscoveryOptions,
OpenIDConnectBackChannelLogoutDiscoveryOptions: p.discovery.OpenIDConnectBackChannelLogoutDiscoveryOptions,
}

options.Issuer = issuer
options.JWKSURI = fmt.Sprintf("%s%s", issuer, JWKsPath)

options.IntrospectionEndpoint = fmt.Sprintf("%s%s", issuer, IntrospectionPath)
options.TokenEndpoint = fmt.Sprintf("%s%s", issuer, TokenPath)

options.AuthorizationEndpoint = fmt.Sprintf("%s%s", issuer, AuthorizationPath)
options.RevocationEndpoint = fmt.Sprintf("%s%s", issuer, RevocationPath)
options.UserinfoEndpoint = fmt.Sprintf("%s%s", issuer, UserinfoPath)

return options
}
Loading

0 comments on commit c9d86a9

Please sign in to comment.