Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for sign in with local credentials #229

Merged
merged 17 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

# testing
/coverage
test.json
test_source.json

# production
/build
Expand Down
21 changes: 19 additions & 2 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: '3.7'
services:
kratos-migrate:
image: oryd/kratos:v1.0.0
image: oryd/kratos:v1.1.0
natalian98 marked this conversation as resolved.
Show resolved Hide resolved
environment:
- DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true&mode=rwc
volumes:
Expand All @@ -19,7 +19,8 @@ services:
kratos:
depends_on:
- kratos-migrate
image: oryd/kratos:v1.0.0
image: oryd/kratos:v1.1.0
container_name: kratos
ports:
- '4433:4433' # public
- '4434:4434' # admin
Expand All @@ -40,6 +41,22 @@ services:
target: /etc/config/kratos
networks:
- intranet
kratos-setup:
image: oryd/kratos:v1.1.0
depends_on:
- kratos
restart: "no"
volumes:
- type: volume
source: kratos-sqlite
target: /var/lib/sqlite
read_only: false
- type: bind
source: ./docker/kratos
target: /etc/config/kratos
command: import identities /etc/config/kratos/identity.json --endpoint http://kratos:4434
networks:
- intranet
hydra:
image: ghcr.io/canonical/hydra:2.2.0-canonical
ports:
Expand Down
15 changes: 15 additions & 0 deletions docker/kratos/identity.json
natalian98 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"traits": {
"email": "test@example.com",
"name": "Test",
"surname": "Example"
},
"schema_id": "default",
"credentials": {
"password": {
"config": {
"password": "test"
}
}
}
}
19 changes: 18 additions & 1 deletion docker/kratos/identity.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,24 @@
"email": {
"type": "string",
"format": "email",
"title": "E-Mail"
"title": "E-Mail",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
},
"totp": {
"account_name": true
}
},
"verification": {
"via": "email"
},
"recovery": {
"via": "email"
}
}
},
"gender": {
"type": "string",
Expand Down
2 changes: 1 addition & 1 deletion docker/kratos/kratos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ selfservice:
- hook: session
methods:
password:
enabled: False
enabled: True
oidc:
enabled: True
config:
Expand Down
4 changes: 2 additions & 2 deletions pkg/kratos/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,10 @@ func (a *API) handleUpdateFlow(w http.ResponseWriter, r *http.Request) {
return
}

flow, cookies, err := a.service.UpdateOIDCLoginFlow(context.Background(), flowId, *body, r.Cookies())
flow, cookies, err := a.service.UpdateLoginFlow(context.Background(), flowId, *body, r.Cookies())
if err != nil {
a.logger.Errorf("Error when updating login flow: %v\n", err)
http.Error(w, "Failed to update login flow", http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/kratos/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ func TestHandleUpdateFlow(t *testing.T) {
req.URL.RawQuery = values.Encode()

mockService.EXPECT().ParseLoginFlowMethodBody(gomock.Any()).Return(flowBody, nil)
mockService.EXPECT().UpdateOIDCLoginFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(redirectFlow, req.Cookies(), nil)
mockService.EXPECT().UpdateLoginFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(redirectFlow, req.Cookies(), nil)
mockService.EXPECT().GetLoginFlow(gomock.Any(), flowId, req.Cookies()).Return(flow, nil, nil)
mockService.EXPECT().CheckAllowedProvider(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil)

Expand Down Expand Up @@ -495,7 +495,7 @@ func TestHandleUpdateFlowFailOnUpdateOIDCLoginFlow(t *testing.T) {
req.URL.RawQuery = values.Encode()

mockService.EXPECT().ParseLoginFlowMethodBody(gomock.Any()).Return(flowBody, nil)
mockService.EXPECT().UpdateOIDCLoginFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(nil, nil, fmt.Errorf("error"))
mockService.EXPECT().UpdateLoginFlow(gomock.Any(), flowId, *flowBody, req.Cookies()).Return(nil, nil, fmt.Errorf("error"))
mockService.EXPECT().GetLoginFlow(gomock.Any(), flowId, req.Cookies()).Return(flow, nil, nil)
mockService.EXPECT().CheckAllowedProvider(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil)
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1)
Expand Down
2 changes: 1 addition & 1 deletion pkg/kratos/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type ServiceInterface interface {
AcceptLoginRequest(context.Context, string, string) (*hClient.OAuth2RedirectTo, []*http.Cookie, error)
CreateBrowserLoginFlow(context.Context, string, string, string, bool, []*http.Cookie) (*kClient.LoginFlow, []*http.Cookie, error)
GetLoginFlow(context.Context, string, []*http.Cookie) (*kClient.LoginFlow, []*http.Cookie, error)
UpdateOIDCLoginFlow(context.Context, string, kClient.UpdateLoginFlowBody, []*http.Cookie) (*BrowserLocationChangeRequired, []*http.Cookie, error)
UpdateLoginFlow(context.Context, string, kClient.UpdateLoginFlowBody, []*http.Cookie) (*BrowserLocationChangeRequired, []*http.Cookie, error)
GetFlowError(context.Context, string) (*kClient.FlowError, []*http.Cookie, error)
CheckAllowedProvider(context.Context, *kClient.LoginFlow, *kClient.UpdateLoginFlowBody) (bool, error)
FilterFlowProviderList(context.Context, *kClient.LoginFlow) (*kClient.LoginFlow, error)
Expand Down
93 changes: 86 additions & 7 deletions pkg/kratos/service.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package kratos

import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand All @@ -14,6 +16,9 @@ import (
kClient "github.com/ory/kratos-client-go"
)

const IncorrectCredentials = 4000006
const InactiveAccount = 4000010

type Service struct {
kratos KratosClientInterface
hydra HydraClientInterface
Expand Down Expand Up @@ -115,7 +120,7 @@ func (s *Service) GetLoginFlow(ctx context.Context, id string, cookies []*http.C
return flow, resp.Cookies(), nil
}

func (s *Service) UpdateOIDCLoginFlow(
func (s *Service) UpdateLoginFlow(
ctx context.Context, flow string, body kClient.UpdateLoginFlowBody, cookies []*http.Cookie,
) (*BrowserLocationChangeRequired, []*http.Cookie, error) {
ctx, span := s.tracer.Start(ctx, "kratos.FrontendApi.UpdateLoginFlow")
Expand All @@ -134,6 +139,8 @@ func (s *Service) UpdateOIDCLoginFlow(
// redirected to.
if err != nil && resp.StatusCode != 422 {
s.logger.Debugf("full HTTP response: %v", resp)
err := s.getUiError(resp.Body)

return nil, nil, err
}

Expand All @@ -152,6 +159,30 @@ func (s *Service) UpdateOIDCLoginFlow(
return &returnToResp, resp.Cookies(), nil
}

func (s *Service) getUiError(responseBody io.ReadCloser) (err error) {
nsklikas marked this conversation as resolved.
Show resolved Hide resolved
errorMessages := new(kClient.LoginFlow)
body, _ := io.ReadAll(responseBody)
json.Unmarshal([]byte(body), &errorMessages)

errorCodes := errorMessages.Ui.Messages
if len(errorCodes) == 0 {
err = fmt.Errorf("error code not found")
natalian98 marked this conversation as resolved.
Show resolved Hide resolved
s.logger.Errorf(err.Error())
return err
}

switch errorCode := errorCodes[0].Id; errorCode {
case IncorrectCredentials:
err = fmt.Errorf("incorrect username or password")
case InactiveAccount:
err = fmt.Errorf("inactive account")
default:
err = fmt.Errorf("unknown error")
s.logger.Debugf("Kratos error code: %v", errorCode)
}
return err
}

func (s *Service) GetFlowError(ctx context.Context, id string) (*kClient.FlowError, []*http.Cookie, error) {
ctx, span := s.tracer.Start(ctx, "kratos.FrontendApi.GetFlowError")
defer span.End()
Expand All @@ -169,7 +200,7 @@ func (s *Service) CheckAllowedProvider(ctx context.Context, loginFlow *kClient.L
ctx, span := s.tracer.Start(ctx, "kratos.Service.CheckAllowedProvider")
defer span.End()

provider := updateFlowBody.UpdateLoginFlowWithOidcMethod.Provider
provider := s.getProviderName(updateFlowBody)
clientName := s.getClientName(loginFlow)

allowedProviders, err := s.authz.ListObjects(ctx, fmt.Sprintf("app:%s", clientName), "allowed_access", "provider")
Expand All @@ -183,6 +214,13 @@ func (s *Service) CheckAllowedProvider(ctx context.Context, loginFlow *kClient.L
return s.contains(allowedProviders, fmt.Sprintf("%v", provider)), nil
}

func (s *Service) getProviderName(updateFlowBody *kClient.UpdateLoginFlowBody) string {
if updateFlowBody.GetActualInstance() == updateFlowBody.UpdateLoginFlowWithOidcMethod {
return updateFlowBody.UpdateLoginFlowWithOidcMethod.Provider
}
return ""
natalian98 marked this conversation as resolved.
Show resolved Hide resolved
}

func (s *Service) getClientName(loginFlow *kClient.LoginFlow) string {
oauth2LoginRequest := loginFlow.Oauth2LoginRequest
if oauth2LoginRequest != nil {
Expand Down Expand Up @@ -224,16 +262,57 @@ func (s *Service) FilterFlowProviderList(ctx context.Context, flow *kClient.Logi
}

func (s *Service) ParseLoginFlowMethodBody(r *http.Request) (*kClient.UpdateLoginFlowBody, error) {
body := new(kClient.UpdateLoginFlowWithOidcMethod)

err := parseBody(r.Body, &body)
type MethodOnly struct {
Method string `json:"method"`
}

// TODO: try to refactor when we bump kratos sdk to 1.x.x
methodOnly := new(MethodOnly)

defer r.Body.Close()
b, err := io.ReadAll(r.Body)

if err != nil {
return nil, errors.New("unable to read body")
}

// replace the body that was consumed
r.Body = io.NopCloser(bytes.NewReader(b))

if err := json.Unmarshal(b, methodOnly); err != nil {
return nil, err
}
ret := kClient.UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(
body,
)

var ret kClient.UpdateLoginFlowBody

switch methodOnly.Method {
case "password":
body := new(kClient.UpdateLoginFlowWithPasswordMethod)

err := parseBody(r.Body, &body)

if err != nil {
return nil, err
}
ret = kClient.UpdateLoginFlowWithPasswordMethodAsUpdateLoginFlowBody(
body,
)
// method field is empty for oidc: https://github.com/ory/kratos/pull/3564
default:
body := new(kClient.UpdateLoginFlowWithOidcMethod)

err := parseBody(r.Body, &body)

if err != nil {
return nil, err
}

ret = kClient.UpdateLoginFlowWithOidcMethodAsUpdateLoginFlowBody(
body,
)
}

return &ret, nil
}

Expand Down
Loading
Loading