hydra/oauth2/oauth2_rop_test.go

163 lines
5.7 KiB
Go

// Copyright © 2022 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package oauth2_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gofrs/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
hydra "github.com/ory/hydra/v2/client"
"github.com/ory/hydra/v2/driver"
"github.com/ory/hydra/v2/driver/config"
"github.com/ory/hydra/v2/flow"
"github.com/ory/hydra/v2/fosite/compose"
"github.com/ory/hydra/v2/fosite/token/jwt"
"github.com/ory/hydra/v2/internal/kratos"
"github.com/ory/hydra/v2/internal/testhelpers"
hydraoauth2 "github.com/ory/hydra/v2/oauth2"
"github.com/ory/hydra/v2/x"
"github.com/ory/x/sqlxx"
)
func TestResourceOwnerPasswordGrant(t *testing.T) {
t.Parallel()
ctx := context.Background()
fakeKratos := kratos.NewFake()
reg := testhelpers.NewRegistryMemory(t,
driver.WithKratosClient(fakeKratos),
driver.WithExtraFositeFactories(compose.OAuth2ResourceOwnerPasswordCredentialsFactory),
)
publicTS, adminTS := testhelpers.NewOAuth2Server(t.Context(), t, reg)
secret := uuid.Must(uuid.NewV4()).String()
audience := sqlxx.StringSliceJSONFormat{"https://aud.example.com"}
client := &hydra.Client{
Secret: secret,
GrantTypes: []string{"password", "refresh_token"},
Scope: "offline",
Audience: audience,
Lifespans: hydra.Lifespans{
PasswordGrantAccessTokenLifespan: x.NullDuration{Duration: 1 * time.Hour, Valid: true},
PasswordGrantRefreshTokenLifespan: x.NullDuration{Duration: 1 * time.Hour, Valid: true},
},
}
require.NoError(t, reg.ClientManager().CreateClient(ctx, client))
oauth2Config := &oauth2.Config{
ClientID: client.GetID(),
ClientSecret: secret,
Endpoint: oauth2.Endpoint{
AuthURL: reg.Config().OAuth2AuthURL(ctx).String(),
TokenURL: reg.Config().OAuth2TokenURL(ctx).String(),
AuthStyle: oauth2.AuthStyleInHeader,
},
Scopes: []string{"offline"},
}
hs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.Header.Get("Content-Type"), "application/json; charset=UTF-8")
assert.Equal(t, r.Header.Get("Authorization"), "Bearer secret value")
var hookReq hydraoauth2.TokenHookRequest
require.NoError(t, json.NewDecoder(r.Body).Decode(&hookReq))
assert.NotEmpty(t, hookReq.Session)
assert.NotEmpty(t, hookReq.Request)
claims := hookReq.Session.Extra
claims["hooked"] = true
if hookReq.Request.GrantTypes[0] == "refresh_token" {
claims["refreshed"] = true
}
hookResp := hydraoauth2.TokenHookResponse{
Session: flow.AcceptOAuth2ConsentRequestSession{
AccessToken: claims,
IDToken: claims,
},
}
w.WriteHeader(http.StatusOK)
require.NoError(t, json.NewEncoder(w).Encode(&hookResp))
}))
defer hs.Close()
reg.Config().MustSet(ctx, config.KeyTokenHook, &config.HookConfig{
URL: hs.URL,
Auth: &config.Auth{
Type: "api_key",
Config: config.AuthConfig{
In: "header",
Name: "Authorization",
Value: "Bearer secret value",
},
},
})
reg.Config().MustSet(ctx, config.KeyAccessTokenStrategy, "jwt")
t.Run("case=get ROP grant token with valid username and password", func(t *testing.T) {
token, err := oauth2Config.PasswordCredentialsToken(ctx, kratos.FakeUsername, kratos.FakePassword)
require.NoError(t, err)
require.NotEmpty(t, token.AccessToken)
// Access token should have hook and identity_id claims
jwtAT, err := jwt.Parse(token.AccessToken, func(token *jwt.Token) (interface{}, error) {
return reg.AccessTokenJWTSigner().GetPublicKey(ctx)
})
require.NoError(t, err)
assert.Equal(t, kratos.FakeUsername, jwtAT.Claims["ext"].(map[string]any)["username"])
assert.Equal(t, kratos.FakeIdentityID, jwtAT.Claims["sub"])
assert.Equal(t, publicTS.URL, jwtAT.Claims["iss"])
assert.True(t, jwtAT.Claims["ext"].(map[string]any)["hooked"].(bool))
assert.ElementsMatch(t, audience, jwtAT.Claims["aud"])
t.Run("case=introspect token", func(t *testing.T) {
// Introspected token should have hook and identity_id claims
i := testhelpers.IntrospectToken(t, token.AccessToken, adminTS)
assert.True(t, i.Get("active").Bool(), "%s", i)
assert.Equal(t, kratos.FakeUsername, i.Get("ext.username").String(), "%s", i)
assert.Equal(t, kratos.FakeIdentityID, i.Get("sub").String(), "%s", i)
assert.True(t, i.Get("ext.hooked").Bool(), "%s", i)
assert.EqualValues(t, oauth2Config.ClientID, i.Get("client_id").String(), "%s", i)
})
t.Run("case=refresh token", func(t *testing.T) {
// Refreshed access token should have hook and identity_id claims
require.NotEmpty(t, token.RefreshToken)
token.Expiry = token.Expiry.Add(-time.Hour * 24)
refreshedToken, err := oauth2Config.TokenSource(context.Background(), token).Token()
require.NoError(t, err)
require.NotEqual(t, token.AccessToken, refreshedToken.AccessToken)
require.NotEqual(t, token.RefreshToken, refreshedToken.RefreshToken)
jwtAT, err := jwt.Parse(refreshedToken.AccessToken, func(token *jwt.Token) (interface{}, error) {
return reg.AccessTokenJWTSigner().GetPublicKey(ctx)
})
require.NoError(t, err)
assert.Equal(t, kratos.FakeIdentityID, jwtAT.Claims["sub"])
assert.Equal(t, kratos.FakeUsername, jwtAT.Claims["ext"].(map[string]any)["username"])
assert.True(t, jwtAT.Claims["ext"].(map[string]any)["hooked"].(bool))
assert.True(t, jwtAT.Claims["ext"].(map[string]any)["refreshed"].(bool))
})
})
t.Run("case=access denied for invalid password", func(t *testing.T) {
_, err := oauth2Config.PasswordCredentialsToken(ctx, kratos.FakeUsername, "invalid")
retrieveError := new(oauth2.RetrieveError)
require.Error(t, err)
require.ErrorAs(t, err, &retrieveError)
assert.Contains(t, retrieveError.ErrorDescription, "Unable to authenticate the provided username and password credentials")
})
}