Skip to content

Commit

Permalink
Merge pull request #229 from canonical/local-idp-login
Browse files Browse the repository at this point in the history
Support for sign in with local credentials
  • Loading branch information
natalian98 authored Apr 26, 2024
2 parents 7315b9d + 63911f8 commit 678976c
Show file tree
Hide file tree
Showing 19 changed files with 337 additions and 46 deletions.
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
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
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) {
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")
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 ""
}

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

0 comments on commit 678976c

Please sign in to comment.