@@ -5,12 +5,18 @@ package server
55
66import (
77 "context"
8+ "crypto"
9+ "fmt"
810 "html/template"
11+ "net"
912 "net/http"
1013 "net/url"
14+ "slices"
15+ "strconv"
1116 "strings"
1217
1318 "github.com/dexidp/dex/server/templates"
19+ "github.com/dexidp/dex/server/tokens"
1420 "github.com/dexidp/dex/storage"
1521)
1622
@@ -179,3 +185,335 @@ func (s *Server) renderError(r *http.Request, w http.ResponseWriter, status int,
179185 s .logger .ErrorContext (r .Context (), "server template error" , "err" , err )
180186 }
181187}
188+
189+ type displayedAuthErr struct {
190+ Status int
191+ Description string
192+ }
193+
194+ func (err * displayedAuthErr ) Error () string {
195+ return err .Description
196+ }
197+
198+ func newDisplayedErr (status int , format string , a ... interface {}) * displayedAuthErr {
199+ return & displayedAuthErr {status , fmt .Sprintf (format , a ... )}
200+ }
201+
202+ // redirectWithError redirects back to the client with an OAuth2 error response.
203+ // Used for prompt=none when login or consent is required.
204+ func (s * Server ) redirectWithError (w http.ResponseWriter , r * http.Request , authReq * storage.AuthRequest , errType , description string ) {
205+ err := & redirectedAuthErr {
206+ State : authReq .State ,
207+ RedirectURI : authReq .RedirectURI ,
208+ Type : errType ,
209+ Description : description ,
210+ }
211+ err .Handler ().ServeHTTP (w , r )
212+ }
213+
214+ // redirectedAuthErr is an error that should be reported back to the client by 302 redirect
215+ type redirectedAuthErr struct {
216+ State string
217+ RedirectURI string
218+ Type string
219+ Description string
220+ }
221+
222+ func (err * redirectedAuthErr ) Error () string {
223+ return err .Description
224+ }
225+
226+ func (err * redirectedAuthErr ) Handler () http.Handler {
227+ hf := func (w http.ResponseWriter , r * http.Request ) {
228+ v := url.Values {}
229+ v .Add ("state" , err .State )
230+ v .Add ("error" , err .Type )
231+ if err .Description != "" {
232+ v .Add ("error_description" , err .Description )
233+ }
234+
235+ // Parse the redirect URI to ensure it's valid before redirecting
236+ u , parseErr := url .Parse (err .RedirectURI )
237+ if parseErr != nil {
238+ // If URI parsing fails, respond with an error instead of redirecting
239+ http .Error (w , "Invalid redirect URI" , http .StatusBadRequest )
240+ return
241+ }
242+
243+ // Add error parameters to the URL
244+ query := u .Query ()
245+ for key , values := range v {
246+ for _ , value := range values {
247+ query .Add (key , value )
248+ }
249+ }
250+ u .RawQuery = query .Encode ()
251+
252+ http .Redirect (w , r , u .String (), http .StatusSeeOther )
253+ }
254+ return http .HandlerFunc (hf )
255+ }
256+
257+ // parseAuthorizationRequest parses the initial request from the OAuth2 client.
258+ // Returns the auth request, the raw subject from id_token_hint (empty if not provided), and any error.
259+ func (s * Server ) parseAuthorizationRequest (r * http.Request ) (* storage.AuthRequest , string , error ) {
260+ ctx := r .Context ()
261+ if err := r .ParseForm (); err != nil {
262+ return nil , "" , newDisplayedErr (http .StatusBadRequest , "Failed to parse request." )
263+ }
264+ q := r .Form
265+ // r.ParseForm already URL-decodes query values once; decoding redirect_uri a
266+ // second time created a normalization differential with the token endpoint.
267+ redirectURI := q .Get ("redirect_uri" )
268+
269+ clientID := q .Get ("client_id" )
270+ state := q .Get ("state" )
271+ nonce := q .Get ("nonce" )
272+ connectorID := q .Get ("connector_id" )
273+ // Some clients, like the old go-oidc, provide extra whitespace. Tolerate this.
274+ scopes := strings .Fields (q .Get ("scope" ))
275+ responseTypes := strings .Fields (q .Get ("response_type" ))
276+
277+ codeChallenge := q .Get ("code_challenge" )
278+ codeChallengeMethod := q .Get ("code_challenge_method" )
279+
280+ if codeChallengeMethod == "" {
281+ codeChallengeMethod = codeChallengeMethodPlain
282+ }
283+
284+ client , err := s .storage .GetClient (ctx , clientID )
285+ if err != nil {
286+ if err == storage .ErrNotFound {
287+ s .logger .ErrorContext (r .Context (), "invalid client_id provided" , "client_id" , clientID )
288+ return nil , "" , newDisplayedErr (http .StatusNotFound , "Invalid client_id." )
289+ }
290+ s .logger .ErrorContext (r .Context (), "failed to get client" , "err" , err )
291+ return nil , "" , newDisplayedErr (http .StatusInternalServerError , "Database error." )
292+ }
293+
294+ if ! validateRedirectURI (client , redirectURI ) {
295+ s .logger .ErrorContext (r .Context (), "unregistered redirect_uri" , "redirect_uri" , redirectURI , "client_id" , clientID )
296+ return nil , "" , newDisplayedErr (http .StatusBadRequest , "Unregistered redirect_uri." )
297+ }
298+ if redirectURI == deviceCallbackURI && client .Public {
299+ redirectURI = s .absPath (deviceCallbackURI )
300+ }
301+
302+ // From here on out, we want to redirect back to the client with an error.
303+ newRedirectedErr := func (typ , format string , a ... interface {}) * redirectedAuthErr {
304+ return & redirectedAuthErr {state , redirectURI , typ , fmt .Sprintf (format , a ... )}
305+ }
306+
307+ if connectorID != "" {
308+ connectors , err := s .storage .ListConnectors (ctx )
309+ if err != nil {
310+ s .logger .ErrorContext (r .Context (), "failed to list connectors" , "err" , err )
311+ return nil , "" , newRedirectedErr (errServerError , "Unable to retrieve connectors" )
312+ }
313+ if ! validateConnectorID (connectors , connectorID ) {
314+ return nil , "" , newRedirectedErr (errInvalidRequest , "Invalid ConnectorID" )
315+ }
316+ if ! isConnectorAllowed (client .AllowedConnectors , connectorID ) {
317+ return nil , "" , newRedirectedErr (errInvalidRequest , "Connector not allowed for this client" )
318+ }
319+ }
320+
321+ // dex doesn't support request parameter and must return request_not_supported error
322+ // https://openid.net/specs/openid-connect-core-1_0.html#6.1
323+ if q .Get ("request" ) != "" {
324+ return nil , "" , newRedirectedErr (errRequestNotSupported , "Server does not support request parameter." )
325+ }
326+
327+ if codeChallenge != "" && ! slices .Contains (s .pkce .CodeChallengeMethodsSupported , codeChallengeMethod ) {
328+ return nil , "" , newRedirectedErr (errInvalidRequest , "Unsupported PKCE challenge method (%q)." , codeChallengeMethod )
329+ }
330+
331+ // Enforce PKCE if configured.
332+ // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12#section-4.1.1
333+ if s .pkce .Enforce && codeChallenge == "" {
334+ return nil , "" , newRedirectedErr (errInvalidRequest , "PKCE is required. The code_challenge parameter must be provided." )
335+ }
336+
337+ var (
338+ unrecognized []string
339+ invalidScopes []string
340+ )
341+ hasOpenIDScope := false
342+ for _ , scope := range scopes {
343+ switch scope {
344+ case tokens .ScopeOpenID :
345+ hasOpenIDScope = true
346+ case tokens .ScopeOfflineAccess , tokens .ScopeEmail , tokens .ScopeProfile , tokens .ScopeGroups , tokens .ScopeFederatedID :
347+ default :
348+ peerID , ok := tokens .ParseCrossClientScope (scope )
349+ if ! ok {
350+ unrecognized = append (unrecognized , scope )
351+ continue
352+ }
353+
354+ isTrusted , err := s .validateCrossClientTrust (r .Context (), clientID , peerID )
355+ if err != nil {
356+ return nil , "" , newRedirectedErr (errServerError , "Internal server error." )
357+ }
358+ if ! isTrusted {
359+ invalidScopes = append (invalidScopes , scope )
360+ }
361+ }
362+ }
363+ if ! hasOpenIDScope {
364+ return nil , "" , newRedirectedErr (errInvalidScope , `Missing required scope(s) ["openid"].` )
365+ }
366+ if len (unrecognized ) > 0 {
367+ return nil , "" , newRedirectedErr (errInvalidScope , "Unrecognized scope(s) %q" , unrecognized )
368+ }
369+ if len (invalidScopes ) > 0 {
370+ return nil , "" , newRedirectedErr (errInvalidScope , "Client can't request scope(s) %q" , invalidScopes )
371+ }
372+
373+ var rt struct {
374+ code bool
375+ idToken bool
376+ token bool
377+ }
378+
379+ for _ , responseType := range responseTypes {
380+ switch responseType {
381+ case responseTypeCode :
382+ rt .code = true
383+ case responseTypeIDToken :
384+ rt .idToken = true
385+ case responseTypeToken :
386+ rt .token = true
387+ default :
388+ return nil , "" , newRedirectedErr (errInvalidRequest , "Invalid response type %q" , responseType )
389+ }
390+
391+ if ! s .supportedResponseTypes [responseType ] {
392+ return nil , "" , newRedirectedErr (errUnsupportedResponseType , "Unsupported response type %q" , responseType )
393+ }
394+ }
395+
396+ if len (responseTypes ) == 0 {
397+ return nil , "" , newRedirectedErr (errInvalidRequest , "No response_type provided" )
398+ }
399+
400+ if rt .token && ! rt .code && ! rt .idToken {
401+ // "token" can't be provided by its own.
402+ //
403+ // https://openid.net/specs/openid-connect-core-1_0.html#Authentication
404+ return nil , "" , newRedirectedErr (errInvalidRequest , "Response type 'token' must be provided with type 'id_token' and/or 'code'" )
405+ }
406+ if ! rt .code {
407+ // Either "id_token token" or "id_token" has been provided which implies the
408+ // implicit flow. Implicit flow requires a nonce value.
409+ //
410+ // https://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthRequest
411+ if nonce == "" {
412+ return nil , "" , newRedirectedErr (errInvalidRequest , "Response type 'token' requires a 'nonce' value." )
413+ }
414+ }
415+ if rt .token {
416+ if redirectURI == redirectURIOOB {
417+ return nil , "" , newRedirectedErr (errInvalidRequest , "Cannot use response type 'token' with redirect_uri '%s'." , redirectURIOOB )
418+ }
419+ }
420+
421+ prompt , err := ParsePrompt (q .Get ("prompt" ))
422+ if err != nil {
423+ return nil , "" , newRedirectedErr (errInvalidRequest , "Invalid prompt parameter: %v" , err )
424+ }
425+
426+ // Parse max_age: -1 means not specified.
427+ maxAge := - 1
428+ if maxAgeStr := q .Get ("max_age" ); maxAgeStr != "" {
429+ v , err := strconv .Atoi (maxAgeStr )
430+ if err != nil || v < 0 {
431+ return nil , "" , newRedirectedErr (errInvalidRequest , "Invalid max_age value %q" , maxAgeStr )
432+ }
433+ maxAge = v
434+ }
435+
436+ // OIDC prompt=consent implies force approval.
437+ forceApproval := q .Get ("approval_prompt" ) == "force" || prompt .Consent ()
438+
439+ // Validate id_token_hint if provided (OIDC Core 1.0 §3.1.2.1).
440+ var idTokenHintSubject string
441+ if hint := q .Get ("id_token_hint" ); hint != "" {
442+ idToken , err := s .validateIDTokenHint (ctx , hint )
443+ if err != nil {
444+ return nil , "" , newRedirectedErr (errInvalidRequest , "Invalid id_token_hint." )
445+ }
446+ idTokenHintSubject = idToken .Subject
447+ }
448+
449+ return & storage.AuthRequest {
450+ ID : storage .NewID (),
451+ ClientID : client .ID ,
452+ State : state ,
453+ Nonce : nonce ,
454+ ForceApprovalPrompt : forceApproval ,
455+ Prompt : prompt .String (),
456+ MaxAge : maxAge ,
457+ Scopes : scopes ,
458+ RedirectURI : redirectURI ,
459+ ResponseTypes : responseTypes ,
460+ ConnectorID : connectorID ,
461+ PKCE : storage.PKCE {
462+ CodeChallenge : codeChallenge ,
463+ CodeChallengeMethod : codeChallengeMethod ,
464+ },
465+ HMACKey : storage .NewHMACKey (crypto .SHA256 ),
466+ }, idTokenHintSubject , nil
467+ }
468+
469+ func validateRedirectURI (client storage.Client , redirectURI string ) bool {
470+ // Allow named RedirectURIs for both public and non-public clients.
471+ // This is required make PKCE-enabled web apps work, when configured as public clients.
472+ for _ , uri := range client .RedirectURIs {
473+ if redirectURI == uri {
474+ return true
475+ }
476+ }
477+ // For non-public clients or when RedirectURIs is set, we allow only explicitly named RedirectURIs.
478+ // Otherwise, we check below for special URIs used for desktop or mobile apps.
479+ if ! client .Public || len (client .RedirectURIs ) > 0 {
480+ return false
481+ }
482+
483+ if redirectURI == redirectURIOOB || redirectURI == deviceCallbackURI {
484+ return true
485+ }
486+
487+ // verify that the host is of form "http://localhost:(port)(path)", "http://localhost(path)" or numeric form like
488+ // "http://127.0.0.1:(port)(path)"
489+ u , err := url .Parse (redirectURI )
490+ if err != nil {
491+ return false
492+ }
493+ if u .Scheme != "http" {
494+ return false
495+ }
496+ return isHostLocal (u .Host )
497+ }
498+
499+ func isHostLocal (host string ) bool {
500+ if host == "localhost" || net .ParseIP (host ).IsLoopback () {
501+ return true
502+ }
503+
504+ host , _ , err := net .SplitHostPort (host )
505+ if err != nil {
506+ return false
507+ }
508+
509+ return host == "localhost" || net .ParseIP (host ).IsLoopback ()
510+ }
511+
512+ func validateConnectorID (connectors []storage.Connector , connectorID string ) bool {
513+ for _ , c := range connectors {
514+ if c .ID == connectorID {
515+ return true
516+ }
517+ }
518+ return false
519+ }
0 commit comments