Skip to content

Commit b1a1598

Browse files
committed
Add per-client external authorize redirect URIs
Allow registered clients to configure external_authorize_redirect_uris in the identifier registration YAML. When set, the OIDC authorize flow redirects to the configured external URL for login instead of the built-in sign-in form. Entries support optional scope prefixes (Scope:URL format) where scope-specific matches take precedence over the default URI.
1 parent e9dd6ff commit b1a1598

4 files changed

Lines changed: 91 additions & 4 deletions

File tree

identifier-registration.yaml.in

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ clients:
2121
# origins:
2222
# - https://my-host:8509
2323

24+
# - id: playground-trusted.js
25+
# name: Trusted OIDC Playground with External Login
26+
# trusted: yes
27+
# application_type: web
28+
# redirect_uris:
29+
# - https://my-host:8509/
30+
# origins:
31+
# - https://my-host:8509
32+
# external_authorize_redirect_uris:
33+
# # Default external login URI used for any scope.
34+
# - https://my-external-login:8443/authorize
35+
# # Scope-specific URI used when the given scope is requested.
36+
# - MyApp.Special:https://my-external-login:8443/authorize-special
37+
2438
# - id: playground-trusted.js
2539
# name: Trusted Insecure OIDC Playground
2640
# trusted: yes

identity/clients/models.go

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"crypto/subtle"
2424
"encoding/base64"
2525
"fmt"
26+
"net/url"
27+
"strings"
2628
"time"
2729

2830
"github.com/golang-jwt/jwt/v5"
@@ -52,7 +54,8 @@ type ClientRegistration struct {
5254
TrustedScopes []string `yaml:"trusted_scopes" json:"-"`
5355
Insecure bool `yaml:"insecure" json:"-"`
5456

55-
ImplicitScopes []string `yaml:"implicit_scopes" json:"-"`
57+
ImplicitScopes []string `yaml:"implicit_scopes" json:"-"`
58+
ExternalAuthorizeRedirectURIs []string `yaml:"external_authorize_redirect_uris,flow" json:"-"`
5659

5760
Dynamic bool `yaml:"-" json:"-"`
5861
IDIssuedAt time.Time `yaml:"-" json:"-"`
@@ -81,6 +84,23 @@ type ClientRegistration struct {
8184
// Validate validates the associated client registration data and returns error
8285
// if the data is not valid.
8386
func (cr *ClientRegistration) Validate() error {
87+
for _, entry := range cr.ExternalAuthorizeRedirectURIs {
88+
uri := entry
89+
// Strip scope prefix if present using the colon heuristic.
90+
if idx := strings.Index(entry, ":"); idx > 0 {
91+
rest := entry[idx+1:]
92+
if !strings.HasPrefix(rest, "//") {
93+
uri = rest
94+
}
95+
}
96+
parsed, err := url.Parse(uri)
97+
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
98+
return fmt.Errorf("invalid external_authorize_redirect_uri: %v", entry)
99+
}
100+
if parsed.Scheme != "https" {
101+
return fmt.Errorf("external_authorize_redirect_uri must use https: %v", entry)
102+
}
103+
}
84104
return nil
85105
}
86106

@@ -200,6 +220,46 @@ func (cr *ClientRegistration) ApplyImplicitScopes(scopes map[string]bool) error
200220
return nil
201221
}
202222

223+
// GetExternalAuthorizeRedirectURI returns the external authorize redirect URI
224+
// for the given scopes. A scope-specific match takes precedence over the
225+
// default. Returns empty string if none is configured.
226+
func (cr *ClientRegistration) GetExternalAuthorizeRedirectURI(scopes map[string]bool) string {
227+
if len(cr.ExternalAuthorizeRedirectURIs) == 0 {
228+
return ""
229+
}
230+
231+
var defaultURI string
232+
scopedURIs := make(map[string]string)
233+
for _, entry := range cr.ExternalAuthorizeRedirectURIs {
234+
if idx := strings.Index(entry, ":"); idx > 0 {
235+
rest := entry[idx+1:]
236+
// If the remainder starts with "//", this is a plain URL with
237+
// no scope prefix (e.g. https://...).
238+
if !strings.HasPrefix(rest, "//") {
239+
scopedURIs[entry[:idx]] = rest
240+
continue
241+
}
242+
}
243+
if defaultURI == "" {
244+
defaultURI = entry
245+
}
246+
}
247+
248+
// Try to find a scope-specific match.
249+
if scopes != nil {
250+
for scope, ok := range scopes {
251+
if !ok {
252+
continue
253+
}
254+
if u, found := scopedURIs[scope]; found {
255+
return u
256+
}
257+
}
258+
}
259+
260+
return defaultURI
261+
}
262+
203263
func (cr *ClientRegistration) makeSecret(secret []byte) (string, string, error) {
204264
// Create random secret. HMAC the client name with it to get the subject.
205265
if secret == nil {

identity/clients/registry.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,9 @@ func NewRegistry(ctx context.Context, trustedURI *url.URL, registrationConfFilep
9494
"trusted": client.Trusted,
9595
"insecure": client.Insecure,
9696
"application_type": client.ApplicationType,
97-
"redirect_uris": client.RedirectURIs,
98-
"origins": client.Origins,
97+
"redirect_uris": client.RedirectURIs,
98+
"origins": client.Origins,
99+
"external_authorize_redirect_uris": client.ExternalAuthorizeRedirectURIs,
99100
}
100101

101102
if validateErr != nil {

identity/managers/identifier.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,18 @@ func (im *IdentifierIdentityManager) RegisterManagers(mgrs *managers.Managers) e
122122
return im.identifier.RegisterManagers(mgrs)
123123
}
124124

125+
// getSignInFormURI returns the sign-in form URI for the given client and
126+
// scopes. If the client has a configured external authorize redirect URI, it
127+
// is returned. Otherwise the default sign-in form URI is used.
128+
func (im *IdentifierIdentityManager) getSignInFormURI(clientID string, scopes map[string]bool) string {
129+
if registration, ok := im.clients.Get(context.Background(), clientID); ok && registration != nil {
130+
if uri := registration.GetExternalAuthorizeRedirectURI(scopes); uri != "" {
131+
return uri
132+
}
133+
}
134+
return im.signInFormURI
135+
}
136+
125137
// Authenticate implements the identity.Manager interface.
126138
func (im *IdentifierIdentityManager) Authenticate(ctx context.Context, rw http.ResponseWriter, req *http.Request, ar *payload.AuthenticationRequest, next identity.Manager) (identity.AuthRecord, error) {
127139
var user *identifierUser
@@ -254,7 +266,7 @@ func (im *IdentifierIdentityManager) Authenticate(ctx context.Context, rw http.R
254266
query.Set("claims_scope", strings.Join(claimsScopes, " "))
255267
}
256268
}
257-
u, _ := url.Parse(im.signInFormURI)
269+
u, _ := url.Parse(im.getSignInFormURI(ar.ClientID, ar.Scopes))
258270
u.RawQuery = query.Encode()
259271
utils.WriteRedirect(rw, http.StatusFound, u, nil, false)
260272

0 commit comments

Comments
 (0)