Skip to content

Commit

Permalink
feat: implement Authenticate() using Kratos
Browse files Browse the repository at this point in the history
This will allow resource owner password grants to authenticate
againts a Kratos user pool.

Note that we still recommend agains using the resource owner password
grant and will not support it by default in Hydra.
  • Loading branch information
hperl committed Oct 13, 2023
1 parent 815e82e commit 91cdd49
Show file tree
Hide file tree
Showing 13 changed files with 81 additions and 11 deletions.
2 changes: 1 addition & 1 deletion client/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ type Filter struct {
type Manager interface {
Storage

Authenticate(ctx context.Context, id string, secret []byte) (*Client, error)
AuthenticateClient(ctx context.Context, id string, secret []byte) (*Client, error)
}

type Storage interface {
Expand Down
4 changes: 2 additions & 2 deletions client/manager_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ func TestHelperClientAuthenticate(k string, m Manager) func(t *testing.T) {
RedirectURIs: []string{"http://redirect"},
}))

c, err := m.Authenticate(ctx, "1234321", []byte("secret1"))
c, err := m.AuthenticateClient(ctx, "1234321", []byte("secret1"))
require.Error(t, err)
require.Nil(t, c)

c, err = m.Authenticate(ctx, "1234321", []byte("secret"))
c, err = m.AuthenticateClient(ctx, "1234321", []byte("secret"))
require.NoError(t, err)
assert.Equal(t, "1234321", c.GetID())
}
Expand Down
7 changes: 7 additions & 0 deletions driver/config/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const (
KeyAdminURL = "urls.self.admin"
KeyIssuerURL = "urls.self.issuer"
KeyIdentityProviderAdminURL = "urls.identity_provider.url"
KeyIdentityProviderPublicURL = "urls.identity_provider.publicUrl"
KeyIdentityProviderHeaders = "urls.identity_provider.headers"
KeyAccessTokenStrategy = "strategies.access_token"
KeyJWTScopeClaimStrategy = "strategies.jwt.scope_claim"
Expand Down Expand Up @@ -415,6 +416,12 @@ func (p *DefaultProvider) KratosAdminURL(ctx context.Context) (*url.URL, bool) {

return u, u != nil
}
func (p *DefaultProvider) KratosPublicURL(ctx context.Context) (*url.URL, bool) {
u := p.getProvider(ctx).RequestURIF(KeyIdentityProviderPublicURL, nil)

return u, u != nil
}

func (p *DefaultProvider) KratosRequestHeader(ctx context.Context) http.Header {
hh := map[string]string{}
if err := p.getProvider(ctx).Unmarshal(KeyIdentityProviderHeaders, &hh); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/httpclient/api_oidc.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion internal/kratos/fake_kratos.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ func NewFake() *FakeKratos {
return &FakeKratos{}
}

func (f *FakeKratos) DisableSession(ctx context.Context, identityProviderSessionID string) error {
func (f *FakeKratos) DisableSession(_ context.Context, identityProviderSessionID string) error {
f.DisableSessionWasCalled = true
f.LastDisabledSession = identityProviderSessionID

return nil
}

func (f *FakeKratos) Authenticate(context.Context, string, string) error {
panic("missing")
}

func (f *FakeKratos) Reset() {
f.DisableSessionWasCalled = false
f.LastDisabledSession = ""
Expand Down
43 changes: 42 additions & 1 deletion internal/kratos/kratos.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"net/url"

"github.com/pkg/errors"
"go.opentelemetry.io/otel/attribute"

"github.com/ory/hydra/v2/driver/config"
Expand All @@ -29,6 +30,7 @@ type (
}
Client interface {
DisableSession(ctx context.Context, identityProviderSessionID string) error
Authenticate(ctx context.Context, name, secret string) error
}
Default struct {
dependencies
Expand All @@ -39,6 +41,37 @@ func New(d dependencies) Client {
return &Default{dependencies: d}
}

func (k *Default) Authenticate(ctx context.Context, name, secret string) (err error) {
ctx, span := k.Tracer(ctx).Tracer().Start(ctx, "kratos.Authenticate")
otelx.End(span, &err)

publicURL, ok := k.Config().KratosPublicURL(ctx)
span.SetAttributes(attribute.String("public_url", fmt.Sprintf("%+v", publicURL)))
if !ok {
span.SetAttributes(attribute.Bool("skipped", true))
span.SetAttributes(attribute.String("reason", "kratos public url not set"))

return errors.New("kratos public url not set")
}

kratos := k.newKratosClient(ctx, publicURL)

flow, _, err := kratos.FrontendApi.CreateNativeLoginFlow(ctx).Execute()
if err != nil {
return err
}

_, _, err = kratos.FrontendApi.UpdateLoginFlow(ctx).Flow(flow.Id).UpdateLoginFlowBody(client.UpdateLoginFlowBody{
UpdateLoginFlowWithPasswordMethod: &client.UpdateLoginFlowWithPasswordMethod{
Method: "password",
Identifier: name,
Password: secret,
},
}).Execute()

return err
}

func (k *Default) DisableSession(ctx context.Context, identityProviderSessionID string) (err error) {
ctx, span := k.Tracer(ctx).Tracer().Start(ctx, "kratos.DisableSession")
otelx.End(span, &err)
Expand Down Expand Up @@ -67,7 +100,6 @@ func (k *Default) DisableSession(ctx context.Context, identityProviderSessionID
_, err = kratos.IdentityApi.DisableSession(ctx, identityProviderSessionID).Execute()

return err

}

func (k *Default) clientConfiguration(ctx context.Context, adminURL *url.URL) *client.Configuration {
Expand All @@ -77,3 +109,12 @@ func (k *Default) clientConfiguration(ctx context.Context, adminURL *url.URL) *c

return configuration
}

func (k *Default) newKratosClient(ctx context.Context, publicURL *url.URL) *client.APIClient {
configuration := k.clientConfiguration(ctx, publicURL)
if header := k.Config().KratosRequestHeader(ctx); header != nil {
configuration.HTTPClient.Transport = httpx.WrapTransportWithHeader(configuration.HTTPClient.Transport, header)
}
kratos := client.NewAPIClient(configuration)
return kratos
}
2 changes: 1 addition & 1 deletion oauth2/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ type oidcUserInfo struct {
// This endpoint returns the payload of the ID Token, including `session.id_token` values, of
// the provided OAuth 2.0 Access Token's consent request.
//
// In the case of authentication error, a WWW-Authenticate header might be set in the response
// In the case of authentication error, a WWW-AuthenticateClient header might be set in the response
// with more information about the error. See [the spec](https://datatracker.ietf.org/doc/html/rfc6750#section-3)
// for more details about header format.
//
Expand Down
2 changes: 2 additions & 0 deletions persistence/sql/persister.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/ory/fosite/storage"
"github.com/ory/hydra/v2/aead"
"github.com/ory/hydra/v2/driver/config"
"github.com/ory/hydra/v2/internal/kratos"
"github.com/ory/hydra/v2/persistence"
"github.com/ory/hydra/v2/x"
"github.com/ory/x/contextx"
Expand Down Expand Up @@ -51,6 +52,7 @@ type (
ClientHasher() fosite.Hasher
KeyCipher() *aead.AESGCM
FlowCipher() *aead.XChaCha20Poly1305
Kratos() kratos.Client
contextx.Provider
x.RegistryLogger
x.TracingProvider
Expand Down
7 changes: 7 additions & 0 deletions persistence/sql/persister_authenticate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package sql

import "context"

func (p *Persister) Authenticate(ctx context.Context, name, secret string) error {
return p.r.Kratos().Authenticate(ctx, name, secret)
}
4 changes: 2 additions & 2 deletions persistence/sql/persister_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ func (p *Persister) UpdateClient(ctx context.Context, cl *client.Client) (err er
})
}

func (p *Persister) Authenticate(ctx context.Context, id string, secret []byte) (_ *client.Client, err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.Authenticate")
func (p *Persister) AuthenticateClient(ctx context.Context, id string, secret []byte) (_ *client.Client, err error) {
ctx, span := p.r.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.AuthenticateClient")
defer otelx.End(span, &err)

c, err := p.GetConcreteClient(ctx, id)
Expand Down
4 changes: 2 additions & 2 deletions persistence/sql/persister_nid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,11 @@ func (s *PersisterTestSuite) TestAuthenticate() {
client := &client.Client{ID: "client-id", Secret: "secret"}
require.NoError(t, r.Persister().CreateClient(s.t1, client))

actual, err := r.Persister().Authenticate(s.t2, "client-id", []byte("secret"))
actual, err := r.Persister().AuthenticateClient(s.t2, "client-id", []byte("secret"))
require.Error(t, err)
require.Nil(t, actual)

actual, err = r.Persister().Authenticate(s.t1, "client-id", []byte("secret"))
actual, err = r.Persister().AuthenticateClient(s.t1, "client-id", []byte("secret"))
require.NoError(t, err)
require.NotNil(t, actual)
})
Expand Down
8 changes: 8 additions & 0 deletions spec/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,14 @@
"https://kratos.example.com/admin"
]
},
"publicUrl": {
"title": "The public URL of the ORY Kratos instance.",
"type": "string",
"format": "uri",
"examples": [
"https://kratos.example.com/public"
]
},
"headers": {
"title": "HTTP Request Headers",
"description": "These headers will be passed in HTTP requests to the Identity Provider.",
Expand Down
1 change: 1 addition & 0 deletions x/fosite_storer.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type FositeStorer interface {
pkce.PKCERequestStorage
rfc7523.RFC7523KeyStorage
verifiable.NonceManager
oauth2.ResourceOwnerPasswordCredentialsGrantStorage

RevokeRefreshToken(ctx context.Context, requestID string) error

Expand Down

0 comments on commit 91cdd49

Please sign in to comment.