snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
70
hydra/fake.go
Normal file
70
hydra/fake.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package hydra
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/ory/herodot"
|
||||
hydraclientgo "github.com/ory/hydra-client-go/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
FakeInvalidLoginChallenge = "2e98454e-031b-4870-9ad6-8517df1ce604"
|
||||
FakeValidLoginChallenge = "5ff59a39-ecc5-467e-bb10-26644c0700ee"
|
||||
FakePostLoginURL = "https://www.example.com/fake-post-login"
|
||||
FakeClientID = "fake-oauth2-client-id"
|
||||
FakeClientName = "Fake OAuth2 Client"
|
||||
FakeClientLogoURI = "https://www.example.com/logo.png"
|
||||
)
|
||||
|
||||
var ErrFakeAcceptLoginRequestFailed = errors.New("failed to accept login request")
|
||||
|
||||
type FakeHydra struct {
|
||||
Skip bool
|
||||
RequestURL string
|
||||
}
|
||||
|
||||
var _ Hydra = &FakeHydra{}
|
||||
|
||||
func NewFake() *FakeHydra {
|
||||
return &FakeHydra{
|
||||
RequestURL: "https://www.ory.com",
|
||||
}
|
||||
}
|
||||
|
||||
func (h *FakeHydra) AcceptLoginRequest(_ context.Context, params AcceptLoginRequestParams) (string, error) {
|
||||
if params.SessionID == "" {
|
||||
return "", errors.New("session id must not be empty")
|
||||
}
|
||||
switch params.LoginChallenge {
|
||||
case FakeInvalidLoginChallenge:
|
||||
return "", ErrFakeAcceptLoginRequestFailed
|
||||
case FakeValidLoginChallenge:
|
||||
return FakePostLoginURL, nil
|
||||
default:
|
||||
panic("unknown fake login_challenge " + params.LoginChallenge)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *FakeHydra) GetLoginRequest(_ context.Context, loginChallenge string) (*hydraclientgo.OAuth2LoginRequest, error) {
|
||||
switch loginChallenge {
|
||||
case FakeInvalidLoginChallenge:
|
||||
return nil, herodot.ErrBadRequest().WithReasonf("Unable to get OAuth 2.0 Login Challenge.")
|
||||
case FakeValidLoginChallenge:
|
||||
return &hydraclientgo.OAuth2LoginRequest{
|
||||
Challenge: loginChallenge,
|
||||
RequestUrl: h.RequestURL,
|
||||
Skip: h.Skip,
|
||||
Client: hydraclientgo.OAuth2Client{
|
||||
ClientId: hydraclientgo.PtrString(FakeClientID),
|
||||
ClientName: hydraclientgo.PtrString(FakeClientName),
|
||||
LogoUri: hydraclientgo.PtrString(FakeClientLogoURI),
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
panic("unknown fake login_challenge " + loginChallenge)
|
||||
}
|
||||
}
|
||||
172
hydra/hydra.go
Normal file
172
hydra/hydra.go
Normal file
@@ -0,0 +1,172 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package hydra
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ory/x/httpx"
|
||||
"github.com/ory/x/sqlxx"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/ory/herodot"
|
||||
hydraclientgo "github.com/ory/hydra-client-go/v2"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/session"
|
||||
)
|
||||
|
||||
type (
|
||||
hydraDependencies interface {
|
||||
config.Provider
|
||||
httpx.ClientProvider
|
||||
}
|
||||
Provider interface {
|
||||
Hydra() Hydra
|
||||
}
|
||||
AcceptLoginRequestParams struct {
|
||||
LoginChallenge string
|
||||
IdentityID string
|
||||
SessionID string
|
||||
AuthenticationMethods session.AuthenticationMethods
|
||||
}
|
||||
Hydra interface {
|
||||
AcceptLoginRequest(ctx context.Context, params AcceptLoginRequestParams) (string, error)
|
||||
GetLoginRequest(ctx context.Context, loginChallenge string) (*hydraclientgo.OAuth2LoginRequest, error)
|
||||
}
|
||||
DefaultHydra struct {
|
||||
d hydraDependencies
|
||||
}
|
||||
)
|
||||
|
||||
func NewDefaultHydra(d hydraDependencies) *DefaultHydra {
|
||||
return &DefaultHydra{
|
||||
d: d,
|
||||
}
|
||||
}
|
||||
|
||||
func GetLoginChallengeID(conf *config.Config, r *http.Request) (sqlxx.NullString, error) {
|
||||
if !r.URL.Query().Has("login_challenge") {
|
||||
return "", nil
|
||||
} else if conf.OAuth2ProviderURL(r.Context()) == nil {
|
||||
return "", errors.WithStack(herodot.ErrMisconfiguration().WithReason("refusing to parse login_challenge query parameter because " + config.ViperKeyOAuth2ProviderURL + " is invalid or unset"))
|
||||
}
|
||||
|
||||
loginChallenge := r.URL.Query().Get("login_challenge")
|
||||
if loginChallenge == "" {
|
||||
return "", errors.WithStack(herodot.ErrBadRequest().WithReason("the login_challenge parameter is present but empty"))
|
||||
}
|
||||
|
||||
return sqlxx.NullString(loginChallenge), nil
|
||||
}
|
||||
|
||||
func (h *DefaultHydra) getAdminURL(ctx context.Context) (string, error) {
|
||||
u := h.d.Config().OAuth2ProviderURL(ctx)
|
||||
if u == nil {
|
||||
return "", errors.WithStack(herodot.ErrMisconfiguration().WithReason(config.ViperKeyOAuth2ProviderURL + " is not configured"))
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func (h *DefaultHydra) getAdminAPIClient(ctx context.Context) (hydraclientgo.OAuth2API, error) {
|
||||
url, err := h.getAdminURL(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
configuration := hydraclientgo.NewConfiguration()
|
||||
configuration.Servers = hydraclientgo.ServerConfigurations{{URL: url}}
|
||||
|
||||
client := h.d.HTTPClient(ctx).StandardClient()
|
||||
if header := h.d.Config().OAuth2ProviderHeader(ctx); header != nil {
|
||||
client.Transport = httpx.WrapTransportWithHeader(client.Transport, header)
|
||||
}
|
||||
|
||||
configuration.HTTPClient = client
|
||||
return hydraclientgo.NewAPIClient(configuration).OAuth2API, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHydra) AcceptLoginRequest(ctx context.Context, params AcceptLoginRequestParams) (string, error) {
|
||||
remember := h.d.Config().SessionPersistentCookie(ctx)
|
||||
rememberFor := int64(h.d.Config().SessionLifespan(ctx) / time.Second)
|
||||
|
||||
alr := hydraclientgo.NewAcceptOAuth2LoginRequest(params.IdentityID)
|
||||
alr.IdentityProviderSessionId = ¶ms.SessionID
|
||||
alr.Remember = &remember
|
||||
alr.RememberFor = &rememberFor
|
||||
alr.Amr = []string{}
|
||||
for _, r := range params.AuthenticationMethods {
|
||||
alr.Amr = append(alr.Amr, string(r.Method))
|
||||
}
|
||||
|
||||
aa, err := h.getAdminAPIClient(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, r, err := aa.AcceptOAuth2LoginRequest(ctx).LoginChallenge(params.LoginChallenge).AcceptOAuth2LoginRequest(*alr).Execute()
|
||||
if err != nil {
|
||||
innerErr := herodot.ErrInternalServerError().WithWrap(err).WithReasonf("Unable to accept OAuth 2.0 Login Challenge.")
|
||||
if r != nil {
|
||||
innerErr = innerErr.
|
||||
WithDetail("status_code", r.StatusCode).
|
||||
WithDebug(err.Error())
|
||||
}
|
||||
|
||||
if openApiErr := new(hydraclientgo.GenericOpenAPIError); errors.As(err, &openApiErr) {
|
||||
switch oauth2Err := openApiErr.Model().(type) {
|
||||
case hydraclientgo.ErrorOAuth2:
|
||||
innerErr = innerErr.WithDetail("oauth2_error_hint", oauth2Err.GetErrorHint())
|
||||
case *hydraclientgo.ErrorOAuth2:
|
||||
innerErr = innerErr.WithDetail("oauth2_error_hint", oauth2Err.GetErrorHint())
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.WithStack(innerErr)
|
||||
}
|
||||
|
||||
return resp.RedirectTo, nil
|
||||
}
|
||||
|
||||
func (h *DefaultHydra) GetLoginRequest(ctx context.Context, loginChallenge string) (*hydraclientgo.OAuth2LoginRequest, error) {
|
||||
if loginChallenge == "" {
|
||||
return nil, errors.WithStack(herodot.ErrBadRequest().WithReason("invalid login_challenge"))
|
||||
}
|
||||
|
||||
aa, err := h.getAdminAPIClient(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hlr, r, err := aa.GetOAuth2LoginRequest(ctx).LoginChallenge(loginChallenge).Execute()
|
||||
if err != nil {
|
||||
var innerErr *herodot.DefaultError
|
||||
if r == nil || r.StatusCode >= 500 {
|
||||
innerErr = herodot.ErrInternalServerError()
|
||||
} else {
|
||||
innerErr = herodot.ErrBadRequest()
|
||||
}
|
||||
innerErr = innerErr.WithReasonf("Unable to get OAuth 2.0 Login Challenge.")
|
||||
if r != nil {
|
||||
innerErr = innerErr.
|
||||
WithDetail("status_code", r.StatusCode).
|
||||
WithDebug(err.Error())
|
||||
}
|
||||
|
||||
if openApiErr := new(hydraclientgo.GenericOpenAPIError); errors.As(err, &openApiErr) {
|
||||
switch oauth2Err := openApiErr.Model().(type) {
|
||||
case hydraclientgo.ErrorOAuth2:
|
||||
innerErr = innerErr.WithDetail("oauth2_error_hint", oauth2Err.GetErrorHint())
|
||||
case *hydraclientgo.ErrorOAuth2:
|
||||
innerErr = innerErr.WithDetail("oauth2_error_hint", oauth2Err.GetErrorHint())
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.WithStack(innerErr)
|
||||
}
|
||||
|
||||
return hlr, nil
|
||||
}
|
||||
105
hydra/hydra_test.go
Normal file
105
hydra/hydra_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package hydra_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/hydra"
|
||||
"github.com/ory/x/configx"
|
||||
"github.com/ory/x/contextx"
|
||||
"github.com/ory/x/logrusx"
|
||||
"github.com/ory/x/sqlxx"
|
||||
"github.com/ory/x/urlx"
|
||||
)
|
||||
|
||||
func requestFromChallenge(s string) *http.Request {
|
||||
return &http.Request{URL: urlx.ParseOrPanic("https://hydra?login_challenge=" + s)}
|
||||
}
|
||||
|
||||
func TestGetLoginChallengeID(t *testing.T) {
|
||||
uuidChallenge := "b346a452-e8fb-4828-8ef8-a4dbc98dc23a"
|
||||
blobChallenge := "1337deadbeefcafe"
|
||||
defaultConfig := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation())
|
||||
configWithHydra := config.MustNew(t, logrusx.New("", ""), &contextx.Default{}, configx.SkipValidation(), configx.WithValues(map[string]interface{}{
|
||||
config.ViperKeyOAuth2ProviderURL: "https://hydra",
|
||||
}))
|
||||
|
||||
type args struct {
|
||||
conf *config.Config
|
||||
r *http.Request
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
assertErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "no login challenge; hydra is not configured",
|
||||
args: args{
|
||||
conf: defaultConfig,
|
||||
r: &http.Request{URL: urlx.ParseOrPanic("https://hydra")},
|
||||
},
|
||||
want: "",
|
||||
assertErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "no login challenge; hydra is configured",
|
||||
args: args{
|
||||
conf: configWithHydra,
|
||||
r: &http.Request{URL: urlx.ParseOrPanic("https://hydra")},
|
||||
},
|
||||
want: "",
|
||||
assertErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "empty login challenge; hydra is configured",
|
||||
args: args{
|
||||
conf: configWithHydra,
|
||||
r: requestFromChallenge(""),
|
||||
},
|
||||
want: "",
|
||||
assertErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "login_challenge is present; Hydra is not configured",
|
||||
args: args{
|
||||
conf: defaultConfig,
|
||||
r: requestFromChallenge(uuidChallenge),
|
||||
},
|
||||
want: "",
|
||||
assertErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "login_challenge is present; hydra is configured",
|
||||
args: args{
|
||||
conf: configWithHydra,
|
||||
r: requestFromChallenge(uuidChallenge),
|
||||
},
|
||||
want: uuidChallenge,
|
||||
assertErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "login_challenge is present & non-uuid; hydra is configured",
|
||||
args: args{
|
||||
conf: configWithHydra,
|
||||
r: requestFromChallenge(blobChallenge),
|
||||
},
|
||||
want: blobChallenge,
|
||||
assertErr: assert.NoError,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := hydra.GetLoginChallengeID(tt.args.conf, tt.args.r)
|
||||
tt.assertErr(t, err)
|
||||
assert.Equal(t, sqlxx.NullString(tt.want), got)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user