Skip to content

Commit

Permalink
Merge pull request #265 from canonical/IAM-976-enforce-mfa
Browse files Browse the repository at this point in the history
IAM-976 Implement MFA toggle and enforcement with redirection to `setup_secure`
  • Loading branch information
BarcoMasile authored Aug 9, 2024
2 parents 9936af3 + c53124b commit 3c8bbb3
Show file tree
Hide file tree
Showing 19 changed files with 394 additions and 408 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ cmd/ui/dist:
@echo "copy dist npm files into cmd/ui folder"
mkdir -p cmd/ui/dist
cp -r $(UI_FOLDER)ui/dist cmd/ui/
.PHONY: cmd/ui/dist

npm-build:
$(MAKE) -C ui/ build
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ At the moment the application is sourcing the following from the environment:
`log.txt`. **Make sure application user has permissions to write**.
- `PORT` - HTTP server port, defaults to `8080`
- `BASE_URL` - the base url that the application will be running on
- `KRATOS_PUBLIC_URL` - address of Kratos APIs
- `KRATOS_PUBLIC_URL` - address of Kratos Public APIs
- `KRATOS_ADMIN_URL` - address of Kratos Admin APIs
- `HYDRA_ADMIN_URL` - address of Hydra admin APIs
- `OPENFGA_API_SCHEME` - the OpenFGA API scheme
- `OPENFGA_API_HOST` - the OpenFGA API host name
- `OPENFGA_STORE_ID` - the OpenFGA store ID to use
- `OPENFGA_MODEL_ID` - the OpenFGA model ID to use. If not specified, a new
model will be created
- `MFA_ENABLED` - whether MFA is enabled and enforced, defaults to true

### Container

Expand Down
8 changes: 4 additions & 4 deletions cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import (
"net/http"
"os"
"os/signal"

"github.com/spf13/cobra"

"syscall"
"time"

"github.com/spf13/cobra"

"github.com/kelseyhightower/envconfig"

authz "github.com/canonical/identity-platform-login-ui/internal/authorization"
Expand Down Expand Up @@ -69,6 +68,7 @@ func serve() {
}

kClient := ik.NewClient(specs.KratosPublicURL, specs.Debug)
kAdminClient := ik.NewClient(specs.KratosAdminURL, specs.Debug)
hClient := ih.NewClient(specs.HydraAdminURL, specs.Debug)

var authzClient authz.AuthzClientInterface
Expand All @@ -85,7 +85,7 @@ func serve() {
panic("Invalid authorization model provided")
}

router := web.NewRouter(kClient, hClient, authorizer, distFS, specs.BaseURL, tracer, monitor, logger)
router := web.NewRouter(kClient, kAdminClient, hClient, authorizer, distFS, specs.MFAEnabled, specs.BaseURL, tracer, monitor, logger)

logger.Infof("Starting server on port %v", specs.Port)

Expand Down
6 changes: 5 additions & 1 deletion docker/kratos/kratos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ identity:
schemas:
- id: default
url: file:///etc/config/kratos/identity.schema.json
session:
whoami:
required_aal: highest_available

selfservice:
allowed_return_urls:
- http://localhost:4455/ui/
Expand Down Expand Up @@ -53,7 +57,7 @@ selfservice:
totp:
enabled: true
config:
issuer: GoogleAuthenticator
issuer: Identity Platform
password:
enabled: True
config:
Expand Down
3 changes: 3 additions & 0 deletions internal/config/specs.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type EnvSpec struct {
BaseURL string `envconfig:"base_url" default:""`

KratosPublicURL string `envconfig:"kratos_public_url"`
KratosAdminURL string `envconfig:"kratos_admin_url"`
HydraAdminURL string `envconfig:"hydra_admin_url"`

ApiScheme string `envconfig:"openfga_api_scheme" default:""`
Expand All @@ -24,6 +25,8 @@ type EnvSpec struct {
StoreId string `envconfig:"openfga_store_id"`
AuthorizationModelId string `envconfig:"openfga_authorization_model_id" default:""`
AuthorizationEnabled bool `envconfig:"authorization_enabled" default:"false"`

MFAEnabled bool `envconfig:"mfa_enabled" default:"true"`
}

type Flags struct {
Expand Down
67 changes: 0 additions & 67 deletions internal/http_meta/http_meta.go

This file was deleted.

4 changes: 4 additions & 0 deletions internal/kratos/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ func (c *Client) FrontendApi() client.FrontendApi {
return c.c.FrontendApi
}

func (c *Client) IdentityApi() client.IdentityApi {
return c.c.IdentityApi
}

func (c *Client) MetadataApi() client.MetadataApi {
return c.c.MetadataApi
}
Expand Down
11 changes: 8 additions & 3 deletions pkg/extra/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package extra
import (
"net/http"

"github.com/canonical/identity-platform-login-ui/internal/logging"
"github.com/go-chi/chi/v5"

"github.com/canonical/identity-platform-login-ui/internal/logging"
"github.com/canonical/identity-platform-login-ui/pkg/kratos"
)

type API struct {
service ServiceInterface
kratos kratos.ServiceInterface

logger logging.LoggerInterface
}
Expand All @@ -19,7 +22,7 @@ func (a *API) RegisterEndpoints(mux *chi.Mux) {

// TODO: Validate response when server error handling is implemented
func (a *API) handleConsent(w http.ResponseWriter, r *http.Request) {
session, err := a.service.CheckSession(r.Context(), r.Cookies())
session, _, err := a.kratos.CheckSession(r.Context(), r.Cookies())

if err != nil {
a.logger.Errorf("error when calling kratos: %s", err)
Expand Down Expand Up @@ -57,12 +60,14 @@ func (a *API) handleConsent(w http.ResponseWriter, r *http.Request) {

w.Write(rr)
w.WriteHeader(http.StatusOK)

}

func NewAPI(service ServiceInterface, logger logging.LoggerInterface) *API {
func NewAPI(service ServiceInterface, kratos kratos.ServiceInterface, logger logging.LoggerInterface) *API {
a := new(API)

a.service = service
a.kratos = kratos

a.logger = logger

Expand Down
22 changes: 14 additions & 8 deletions pkg/extra/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (

hClient "github.com/ory/hydra-client-go/v2"
kClient "github.com/ory/kratos-client-go"

"github.com/canonical/identity-platform-login-ui/pkg/kratos"
)

//go:generate mockgen -build_flags=--mod=mod -package extra -destination ./mock_logger.go -source=../../internal/logging/interfaces.go
Expand All @@ -24,6 +26,7 @@ func TestHandleConsentSuccess(t *testing.T) {

mockLogger := NewMockLoggerInterface(ctrl)
mockService := NewMockServiceInterface(ctrl)
mockKratosService := kratos.NewMockServiceInterface(ctrl)

session := kClient.NewSession("test", *kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}))
consent := hClient.NewOAuth2ConsentRequest("challenge")
Expand All @@ -37,12 +40,12 @@ func TestHandleConsentSuccess(t *testing.T) {

w := httptest.NewRecorder()

mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil)
mockKratosService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil, nil)
mockService.EXPECT().GetConsent(gomock.Any(), "7bb518c4eec2454dbb289f5fdb4c0ee2").Return(consent, nil)
mockService.EXPECT().AcceptConsent(gomock.Any(), session.Identity, consent).Return(accept, nil)

mux := chi.NewMux()
NewAPI(mockService, mockLogger).RegisterEndpoints(mux)
NewAPI(mockService, mockKratosService, mockLogger).RegisterEndpoints(mux)

mux.ServeHTTP(w, req)

Expand Down Expand Up @@ -75,6 +78,7 @@ func TestHandleConsentFailOnAcceptConsent(t *testing.T) {

mockLogger := NewMockLoggerInterface(ctrl)
mockService := NewMockServiceInterface(ctrl)
mockKratosService := kratos.NewMockServiceInterface(ctrl)

session := kClient.NewSession("test", *kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}))
consent := hClient.NewOAuth2ConsentRequest("challenge")
Expand All @@ -87,13 +91,13 @@ func TestHandleConsentFailOnAcceptConsent(t *testing.T) {

w := httptest.NewRecorder()

mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil)
mockKratosService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil, nil)
mockService.EXPECT().GetConsent(gomock.Any(), "7bb518c4eec2454dbb289f5fdb4c0ee2").Return(consent, nil)
mockService.EXPECT().AcceptConsent(gomock.Any(), session.Identity, consent).Return(nil, fmt.Errorf("error"))
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1)

mux := chi.NewMux()
NewAPI(mockService, mockLogger).RegisterEndpoints(mux)
NewAPI(mockService, mockKratosService, mockLogger).RegisterEndpoints(mux)

mux.ServeHTTP(w, req)

Expand All @@ -110,6 +114,7 @@ func TestHandleConsentFailOnGetConsent(t *testing.T) {

mockLogger := NewMockLoggerInterface(ctrl)
mockService := NewMockServiceInterface(ctrl)
mockKratosService := kratos.NewMockServiceInterface(ctrl)

session := kClient.NewSession("test", *kClient.NewIdentity("test", "test.json", "https://test.com/test.json", map[string]string{"name": "name"}))

Expand All @@ -121,12 +126,12 @@ func TestHandleConsentFailOnGetConsent(t *testing.T) {

w := httptest.NewRecorder()

mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil)
mockKratosService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(session, nil, nil)
mockService.EXPECT().GetConsent(gomock.Any(), "7bb518c4eec2454dbb289f5fdb4c0ee2").Return(nil, fmt.Errorf("error"))
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1)

mux := chi.NewMux()
NewAPI(mockService, mockLogger).RegisterEndpoints(mux)
NewAPI(mockService, mockKratosService, mockLogger).RegisterEndpoints(mux)

mux.ServeHTTP(w, req)

Expand All @@ -143,6 +148,7 @@ func TestHandleConsentFailOnCheckSession(t *testing.T) {

mockLogger := NewMockLoggerInterface(ctrl)
mockService := NewMockServiceInterface(ctrl)
mockKratosService := kratos.NewMockServiceInterface(ctrl)

req := httptest.NewRequest(http.MethodGet, "/api/consent", nil)

Expand All @@ -152,11 +158,11 @@ func TestHandleConsentFailOnCheckSession(t *testing.T) {

w := httptest.NewRecorder()

mockService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(nil, fmt.Errorf("error"))
mockKratosService.EXPECT().CheckSession(gomock.Any(), req.Cookies()).Return(nil, nil, fmt.Errorf("error"))
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1)

mux := chi.NewMux()
NewAPI(mockService, mockLogger).RegisterEndpoints(mux)
NewAPI(mockService, mockKratosService, mockLogger).RegisterEndpoints(mux)

mux.ServeHTTP(w, req)

Expand Down
6 changes: 0 additions & 6 deletions pkg/extra/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,18 @@ package extra

import (
"context"
"net/http"

hClient "github.com/ory/hydra-client-go/v2"
kClient "github.com/ory/kratos-client-go"

"github.com/canonical/identity-platform-login-ui/internal/hydra"
)

type KratosClientInterface interface {
FrontendApi() kClient.FrontendApi
}

type HydraClientInterface interface {
OAuth2Api() hydra.OAuth2Api
}

type ServiceInterface interface {
CheckSession(context.Context, []*http.Cookie) (*kClient.Session, error)
GetConsent(context.Context, string) (*hClient.OAuth2ConsentRequest, error)
AcceptConsent(context.Context, kClient.Identity, *hClient.OAuth2ConsentRequest) (*hClient.OAuth2RedirectTo, error)
}
Loading

0 comments on commit 3c8bbb3

Please sign in to comment.