diff --git a/example/server/exampleop/op.go b/example/server/exampleop/op.go index 1dc8bd1..7254585 100644 --- a/example/server/exampleop/op.go +++ b/example/server/exampleop/op.go @@ -34,7 +34,7 @@ type Storage interface { // SetupServer creates an OIDC server with Issuer=http://localhost: // // Use one of the pre-made clients in storage/clients.go or register a new one. -func SetupServer(issuer string, storage Storage) *mux.Router { +func SetupServer(issuer string, storage Storage, extraOptions ...op.Option) *mux.Router { // the OpenID Provider requires a 32-byte key for (token) encryption // be sure to create a proper crypto random key and manage it securely! key := sha256.Sum256([]byte("test")) @@ -50,7 +50,7 @@ func SetupServer(issuer string, storage Storage) *mux.Router { }) // creation of the OpenIDProvider with the just created in-memory Storage - provider, err := newOP(storage, issuer, key) + provider, err := newOP(storage, issuer, key, extraOptions...) if err != nil { log.Fatal(err) } @@ -79,7 +79,7 @@ func SetupServer(issuer string, storage Storage) *mux.Router { // newOP will create an OpenID Provider for localhost on a specified port with a given encryption key // and a predefined default logout uri // it will enable all options (see descriptions) -func newOP(storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, error) { +func newOP(storage op.Storage, issuer string, key [32]byte, extraOptions ...op.Option) (op.OpenIDProvider, error) { config := &op.Config{ CryptoKey: key, @@ -112,10 +112,12 @@ func newOP(storage op.Storage, issuer string, key [32]byte) (op.OpenIDProvider, }, } handler, err := op.NewOpenIDProvider(issuer, config, storage, - //we must explicitly allow the use of the http issuer - op.WithAllowInsecure(), - // as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth - op.WithCustomAuthEndpoint(op.NewEndpoint("auth")), + append([]op.Option{ + // we must explicitly allow the use of the http issuer + op.WithAllowInsecure(), + // as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth + op.WithCustomAuthEndpoint(op.NewEndpoint("auth")), + }, extraOptions...)..., ) if err != nil { return nil, err diff --git a/example/server/storage/storage.go b/example/server/storage/storage.go index a4c4f46..406300b 100644 --- a/example/server/storage/storage.go +++ b/example/server/storage/storage.go @@ -28,8 +28,10 @@ var serviceKey1 = &rsa.PublicKey{ E: 65537, } -var _ op.Storage = &Storage{} -var _ op.ClientCredentialsStorage = &Storage{} +var ( + _ op.Storage = &Storage{} + _ op.ClientCredentialsStorage = &Storage{} +) // storage implements the op.Storage interface // typically you would implement this as a layer on top of your database @@ -167,6 +169,12 @@ func (s *Storage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthReque s.lock.Lock() defer s.lock.Unlock() + if len(authReq.Prompt) == 1 && authReq.Prompt[0] == "none" { + // With prompt=none, there is no way for the user to log in + // so return error right away. + return nil, oidc.ErrLoginRequired() + } + // typically, you'll fill your storage / storage model with the information of the passed object request := authRequestToInternal(authReq, userID) diff --git a/pkg/client/integration_test.go b/pkg/client/integration_test.go index 40e1bee..ea7225d 100644 --- a/pkg/client/integration_test.go +++ b/pkg/client/integration_test.go @@ -25,6 +25,7 @@ import ( "github.com/zitadel/oidc/v2/pkg/client/tokenexchange" httphelper "github.com/zitadel/oidc/v2/pkg/http" "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" ) func TestRelyingPartySession(t *testing.T) { @@ -280,6 +281,92 @@ func RunAuthorizationCodeFlow(t *testing.T, opServer *httptest.Server, clientID, return provider, accessToken, refreshToken, idToken } +func TestErrorFromPromptNone(t *testing.T) { + jar, err := cookiejar.New(nil) + require.NoError(t, err, "create cookie jar") + httpClient := &http.Client{ + Timeout: time.Second * 5, + CheckRedirect: func(_ *http.Request, _ []*http.Request) error { + return http.ErrUseLastResponse + }, + Jar: jar, + } + + t.Log("------- start example OP ------") + targetURL := "http://local-site" + exampleStorage := storage.NewStorage(storage.NewUserStore(targetURL)) + var dh deferredHandler + opServer := httptest.NewServer(&dh) + defer opServer.Close() + t.Logf("auth server at %s", opServer.URL) + dh.Handler = exampleop.SetupServer(opServer.URL, exampleStorage, op.WithHttpInterceptors( + func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("request to %s", r.URL) + next.ServeHTTP(w, r) + }) + }, + )) + seed := rand.New(rand.NewSource(int64(os.Getpid()) + time.Now().UnixNano())) + clientID := t.Name() + "-" + strconv.FormatInt(seed.Int63(), 25) + clientSecret := "secret" + client := storage.WebClient(clientID, clientSecret, targetURL) + storage.RegisterClients(client) + + t.Log("------- create RP ------") + key := []byte("test1234test1234") + cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure()) + provider, err := rp.NewRelyingPartyOIDC( + opServer.URL, + clientID, + clientSecret, + targetURL, + []string{"openid", "email", "profile", "offline_access"}, + rp.WithPKCE(cookieHandler), + rp.WithVerifierOpts( + rp.WithIssuedAtOffset(5*time.Second), + rp.WithSupportedSigningAlgorithms("RS256", "RS384", "RS512", "ES256", "ES384", "ES512"), + ), + ) + require.NoError(t, err, "new rp") + + t.Log("------- start auth flow with prompt=none ------- ") + state := "state-32892" + capturedW := httptest.NewRecorder() + localURL, err := url.Parse(targetURL + "/login") + require.NoError(t, err) + + get := httptest.NewRequest("GET", localURL.String(), nil) + rp.AuthURLHandler(func() string { return state }, provider, + rp.WithPromptURLParam("none"), + rp.WithResponseModeURLParam(oidc.ResponseModeFragment), + )(capturedW, get) + + defer func() { + if t.Failed() { + t.Log("response body (redirect from RP to OP)", capturedW.Body.String()) + } + }() + require.GreaterOrEqual(t, capturedW.Code, 200, "captured response code") + require.Less(t, capturedW.Code, 400, "captured response code") + + //nolint:bodyclose + resp := capturedW.Result() + jar.SetCookies(localURL, resp.Cookies()) + + startAuthURL, err := resp.Location() + require.NoError(t, err, "get redirect") + assert.NotEmpty(t, startAuthURL, "login url") + t.Log("Starting auth at", startAuthURL) + + t.Log("------- get redirect from OP ------") + loginPageURL := getRedirect(t, "get redirect to login page", httpClient, startAuthURL) + t.Log("login page URL", loginPageURL) + + require.Contains(t, loginPageURL.String(), `error=login_required`, "prompt=none should error") + require.Contains(t, loginPageURL.String(), `local-site#error=`, "response_mode=fragment means '#' instead of '?'") +} + type deferredHandler struct { http.Handler } diff --git a/pkg/client/rp/relying_party.go b/pkg/client/rp/relying_party.go index 108fa4f..114599d 100644 --- a/pkg/client/rp/relying_party.go +++ b/pkg/client/rp/relying_party.go @@ -569,6 +569,11 @@ func WithPromptURLParam(prompt ...string) URLParamOpt { return withPrompt(prompt...) } +// WithResponseModeURLParam sets the `response_mode` parameter in a URL. +func WithResponseModeURLParam(mode oidc.ResponseMode) URLParamOpt { + return withURLParam("response_mode", string(mode)) +} + type AuthURLOpt func() []oauth2.AuthCodeOption // WithCodeChallenge sets the `code_challenge` params in the auth request diff --git a/pkg/oidc/authorization.go b/pkg/oidc/authorization.go index f620ecb..ace1de1 100644 --- a/pkg/oidc/authorization.go +++ b/pkg/oidc/authorization.go @@ -60,7 +60,7 @@ const ( ) // AuthRequest according to: -//https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest +// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest type AuthRequest struct { Scopes SpaceDelimitedArray `json:"scope" schema:"scope"` ResponseType ResponseType `json:"response_type" schema:"response_type"` @@ -100,3 +100,8 @@ func (a *AuthRequest) GetResponseType() ResponseType { func (a *AuthRequest) GetState() string { return a.State } + +// GetResponseMode returns the optional ResponseMode +func (a *AuthRequest) GetResponseMode() ResponseMode { + return a.ResponseMode +}