diff --git a/config-oidc-conformance.yml b/config-oidc-conformance.yml index edf8672..3d6ded0 100644 --- a/config-oidc-conformance.yml +++ b/config-oidc-conformance.yml @@ -16,6 +16,7 @@ clients: refreshTTL: 45 idTTL: 45 oidc: true + passwordFallbackAllowed: true privateKey: ./.test_files/ecdsa521key.pem redirects: - https://localhost.emobix.co.uk:8443/test/a/instructions-example/callback diff --git a/config.yml b/config.yml index 34a32ef..e11fc0c 100644 --- a/config.yml +++ b/config.yml @@ -32,6 +32,7 @@ clients: idTTL: 15 oidc: true introspect: true + passwordFallbackAllowed: true redirects: - https://oauth.pstmn.io/v1/callback - http://localhost:8080/session/callback diff --git a/internal/server/handler/authorize/authorize.go b/internal/server/handler/authorize/authorize.go index e84ff31..dab167a 100644 --- a/internal/server/handler/authorize/authorize.go +++ b/internal/server/handler/authorize/authorize.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "github.com/google/uuid" + "github.com/lestrrat-go/jwx/v2/jwt" "github.com/webishdev/stopnik/internal/config" "github.com/webishdev/stopnik/internal/endpoint" internalHttp "github.com/webishdev/stopnik/internal/http" @@ -72,12 +73,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { h.handleGetRequest(w, r) } else if r.Method == http.MethodPost { - user, failed := h.validateLogin(w, r) - if failed { - return - } - - h.handlePostRequest(w, r, user) + h.handlePostRequest(w, r) } else { h.errorHandler.MethodNotAllowedHandler(w, r) return @@ -85,96 +81,169 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (h *Handler) handleGetRequest(w http.ResponseWriter, r *http.Request) { - // OAuth2 - clientIdParameter := r.URL.Query().Get(oauth2.ParameterClientId) - stateParameter := r.URL.Query().Get(oauth2.ParameterState) - responseTypeParameter := r.URL.Query().Get(oauth2.ParameterResponseType) - redirectParameter := r.URL.Query().Get(oauth2.ParameterRedirectUri) - scopeParameter := r.URL.Query().Get(oauth2.ParameterScope) - - // PKCE - codeChallengeParameter := r.URL.Query().Get(pkce.ParameterCodeChallenge) - codeChallengeMethodParameter := r.URL.Query().Get(pkce.ParameterCodeChallengeMethod) - - // OpenId Connect - nonceParameter := r.URL.Query().Get(oidc.ParameterNonce) - promptParameter := r.URL.Query().Get(oidc.ParameterPrompt) - maxAgeParameter := r.URL.Query().Get(oidc.ParameterMaxAge) - - authorizeRequest := &authorizeRequestValues{ - clientIdParameter: clientIdParameter, - redirectParameter: redirectParameter, - responseTypeParameter: responseTypeParameter, - stateParameter: stateParameter, - codeChallengeParameter: codeChallengeParameter, - codeChallengeMethodParameter: codeChallengeMethodParameter, - scopeParameter: scopeParameter, - nonceParameter: nonceParameter, - promptParameter: promptParameter, - maxAgeParameter: maxAgeParameter, - } + requestParameter := r.URL.Query().Get("request") + if requestParameter != "" { + parse, err := jwt.Parse([]byte(requestParameter), jwt.WithVerify(false)) + if err != nil { + log.Error("%v", err) + } else { + log.Info("%v", parse) + } - h.handleAuthorizeRequest(w, r, authorizeRequest) -} + scopeParameter, _ := parse.Get("scope") + responseTypeParameter, _ := parse.Get("response_type") + redirectParameter, _ := parse.Get("redirect_uri") + stateParameter, _ := parse.Get("state") + nonceParameter, _ := parse.Get("nonce") + clientIdParameter, _ := parse.Get("client_id") + + authorizeRequest := &authorizeRequestValues{ + clientIdParameter: fmt.Sprintf("%s", clientIdParameter), + redirectParameter: fmt.Sprintf("%s", redirectParameter), + responseTypeParameter: fmt.Sprintf("%s", responseTypeParameter), + stateParameter: fmt.Sprintf("%s", stateParameter), + codeChallengeParameter: "", + codeChallengeMethodParameter: "", + scopeParameter: fmt.Sprintf("%s", scopeParameter), + nonceParameter: fmt.Sprintf("%s", nonceParameter), + promptParameter: "", + maxAgeParameter: "", + } -func (h *Handler) handlePostRequest(w http.ResponseWriter, r *http.Request, user *config.User) { - loginSession := &session.LoginSession{ - Id: uuid.NewString(), - Username: user.Username, - } - h.loginSessionManager.StartSession(loginSession) - authCookie, authCookieError := h.cookieManager.CreateAuthCookie(user.Username, loginSession.Id) - if authCookieError != nil { - h.errorHandler.InternalServerErrorHandler(w, r, authCookieError) - return - } + h.handleAuthorizeRequest(w, r, authorizeRequest) + } else { - authSessionForm := r.PostFormValue("stopnik_auth_session") - loginToken, loginTokenError := h.validator.GetLoginToken(authSessionForm) - if loginTokenError != nil { - h.errorHandler.BadRequestHandler(w, r) - return - } - authSessionId := loginToken.Subject() - authSession, authSessionExists := h.authSessionManager.GetSession(authSessionId) - if !authSessionExists { - h.sendRetryLocation(w, r, "") - return - } + // OAuth2 + clientIdParameter := r.URL.Query().Get(oauth2.ParameterClientId) + stateParameter := r.URL.Query().Get(oauth2.ParameterState) + responseTypeParameter := r.URL.Query().Get(oauth2.ParameterResponseType) + redirectParameter := r.URL.Query().Get(oauth2.ParameterRedirectUri) + scopeParameter := r.URL.Query().Get(oauth2.ParameterScope) + + // PKCE + codeChallengeParameter := r.URL.Query().Get(pkce.ParameterCodeChallenge) + codeChallengeMethodParameter := r.URL.Query().Get(pkce.ParameterCodeChallengeMethod) + + // OpenId Connect + nonceParameter := r.URL.Query().Get(oidc.ParameterNonce) + promptParameter := r.URL.Query().Get(oidc.ParameterPrompt) + maxAgeParameter := r.URL.Query().Get(oidc.ParameterMaxAge) + + authorizeRequest := &authorizeRequestValues{ + clientIdParameter: clientIdParameter, + redirectParameter: redirectParameter, + responseTypeParameter: responseTypeParameter, + stateParameter: stateParameter, + codeChallengeParameter: codeChallengeParameter, + codeChallengeMethodParameter: codeChallengeMethodParameter, + scopeParameter: scopeParameter, + nonceParameter: nonceParameter, + promptParameter: promptParameter, + maxAgeParameter: maxAgeParameter, + } - authSession.Username = user.Username - authSession.AuthTime = loginSession.StartTime - redirectURL, urlParseError := url.Parse(authSession.Redirect) - if urlParseError != nil { - h.errorHandler.InternalServerErrorHandler(w, r, urlParseError) - return + h.handleAuthorizeRequest(w, r, authorizeRequest) } +} - http.SetCookie(w, &authCookie) +func (h *Handler) handlePostRequest(w http.ResponseWriter, r *http.Request) { + authSessionForm := r.PostFormValue("stopnik_auth_session") + if authSessionForm != "" { + user, loginError := h.validator.ValidateFormLogin(r) + if loginError != nil { + h.sendRetryLocation(w, r, *loginError) + return + } - responseTypes := authSession.ResponseTypes + loginSession := &session.LoginSession{ + Id: uuid.NewString(), + Username: user.Username, + } + h.loginSessionManager.StartSession(loginSession) + authCookie, authCookieError := h.cookieManager.CreateAuthCookie(user.Username, loginSession.Id) + if authCookieError != nil { + h.errorHandler.InternalServerErrorHandler(w, r, authCookieError) + return + } - query := redirectURL.Query() - if slices.Contains(responseTypes, oauth2.RtToken) { - client, clientExists := h.validator.ValidateClientId(authSession.ClientId) - if !clientExists { + loginToken, loginTokenError := h.validator.GetLoginToken(authSessionForm) + if loginTokenError != nil { h.errorHandler.BadRequestHandler(w, r) return } - accessTokenResponse := h.tokenManager.CreateAccessTokenResponse(r, user.Username, client, &loginSession.StartTime, authSession.Scopes, authSession.Nonce, "") - setImplicitGrantParameter(query, accessTokenResponse) - } else if slices.Contains(responseTypes, oauth2.RtCode) { - setAuthorizationGrantParameter(query, authSession.Id) + authSessionId := loginToken.Subject() + authSession, authSessionExists := h.authSessionManager.GetSession(authSessionId) + if !authSessionExists { + h.sendRetryLocation(w, r, "") + return + } + + authSession.Username = user.Username + authSession.AuthTime = loginSession.StartTime + redirectURL, urlParseError := url.Parse(authSession.Redirect) + if urlParseError != nil { + h.errorHandler.InternalServerErrorHandler(w, r, urlParseError) + return + } + + http.SetCookie(w, &authCookie) + + responseTypes := authSession.ResponseTypes + + query := redirectURL.Query() + if slices.Contains(responseTypes, oauth2.RtToken) { + client, clientExists := h.validator.ValidateClientId(authSession.ClientId) + if !clientExists { + h.errorHandler.BadRequestHandler(w, r) + return + } + accessTokenResponse := h.tokenManager.CreateAccessTokenResponse(r, user.Username, client, &loginSession.StartTime, authSession.Scopes, authSession.Nonce, "") + setImplicitGrantParameter(query, accessTokenResponse) + } else if slices.Contains(responseTypes, oauth2.RtCode) { + setAuthorizationGrantParameter(query, authSession.Id) + } else { + oauth2.AuthorizationErrorResponseHandler(w, redirectURL, authSession.State, &oauth2.AuthorizationErrorResponseParameter{Error: oauth2.AuthorizationEtUnsupportedResponseType}) + return + } + + if authSession.State != "" { + query.Set(oauth2.ParameterState, authSession.State) + } + + sendFound(w, redirectURL, query) } else { - oauth2.AuthorizationErrorResponseHandler(w, redirectURL, authSession.State, &oauth2.AuthorizationErrorResponseParameter{Error: oauth2.AuthorizationEtUnsupportedResponseType}) - return - } + // OAuth2 + clientIdParameter := r.PostFormValue(oauth2.ParameterClientId) + stateParameter := r.PostFormValue(oauth2.ParameterState) + responseTypeParameter := r.PostFormValue(oauth2.ParameterResponseType) + redirectParameter := r.PostFormValue(oauth2.ParameterRedirectUri) + scopeParameter := r.PostFormValue(oauth2.ParameterScope) + + // PKCE + codeChallengeParameter := r.PostFormValue(pkce.ParameterCodeChallenge) + codeChallengeMethodParameter := r.PostFormValue(pkce.ParameterCodeChallengeMethod) + + // OpenId Connect + nonceParameter := r.PostFormValue(oidc.ParameterNonce) + promptParameter := r.PostFormValue(oidc.ParameterPrompt) + maxAgeParameter := r.PostFormValue(oidc.ParameterMaxAge) + + authorizeRequest := &authorizeRequestValues{ + clientIdParameter: clientIdParameter, + redirectParameter: redirectParameter, + responseTypeParameter: responseTypeParameter, + stateParameter: stateParameter, + codeChallengeParameter: codeChallengeParameter, + codeChallengeMethodParameter: codeChallengeMethodParameter, + scopeParameter: scopeParameter, + nonceParameter: nonceParameter, + promptParameter: promptParameter, + maxAgeParameter: maxAgeParameter, + } - if authSession.State != "" { - query.Set(oauth2.ParameterState, authSession.State) + h.handleAuthorizeRequest(w, r, authorizeRequest) } - sendFound(w, redirectURL, query) } func (h *Handler) handleAuthorizeRequest(w http.ResponseWriter, r *http.Request, authorizeRequest *authorizeRequestValues) { @@ -302,16 +371,6 @@ func (h *Handler) handleAuthorizeRequest(w http.ResponseWriter, r *http.Request, } } -func (h *Handler) validateLogin(w http.ResponseWriter, r *http.Request) (*config.User, bool) { - // Handle Post from Login - user, loginError := h.validator.ValidateFormLogin(r) - if loginError != nil { - h.sendRetryLocation(w, r, *loginError) - return nil, true - } - return user, false -} - func (h *Handler) validateRedirect(client *config.Client, redirect string) func(w http.ResponseWriter, r *http.Request) { if redirect == "" { log.Error("Redirect provided for client %s was empty", client.Id) @@ -431,10 +490,7 @@ func setIdTokenParameter(query url.Values, idToken string) { func (h *Handler) sendLogin(w http.ResponseWriter, r *http.Request, authSessionId string) { message := h.cookieManager.GetMessageCookieValue(r) - query := r.URL.Query() - encodedQuery := query.Encode() - authorization := endpoint.Authorization[1:] - formAction := fmt.Sprintf("%s?%s", authorization, encodedQuery) + formAction := endpoint.Authorization[1:] loginToken := h.validator.NewLoginToken(authSessionId) loginTemplate := h.templateManager.LoginTemplate(loginToken, formAction, message) diff --git a/internal/server/handler/oidc/discovery.go b/internal/server/handler/oidc/discovery.go index 3c3b407..df855fd 100644 --- a/internal/server/handler/oidc/discovery.go +++ b/internal/server/handler/oidc/discovery.go @@ -39,7 +39,7 @@ type oidcConfigurationResponse struct { ClaimsSupported []string `json:"claims_supported,omitempty"` ClaimsLocalesSupported []string `json:"claims_locales_supported,omitempty"` ClaimsParameterSupported []string `json:"claims_parameter_supported,omitempty"` - RequestParameterSupported []string `json:"request_parameter_supported,omitempty"` + RequestParameterSupported bool `json:"request_parameter_supported,omitempty"` RequestUriParameterSupported []string `json:"request_uri_parameter_supported,omitempty"` RequireRequestUriRegistration []string `json:"require_request_uri_registration,omitempty"` TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported,omitempty"` @@ -137,6 +137,7 @@ func (h *DiscoveryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { RevocationEndpointAuthMethodsSupported: authMethodsSupported, RevocationEndpointAuthSigningAlgValuesSupported: signatureAlgorithmSupported, IdTokenSigningAlgValuesSupported: signatureAlgorithmSupported, + RequestParameterSupported: true, SubjectTypesSupported: []string{"public"}, ScopesSupported: []string{oidc.ScopeOpenId, oidc.ScopeProfile, oidc.ScopeAddress, oidc.ScopeEmail, oidc.ScopePhone, oidc.ScopeOfflineAccess}, }