Skip to content

Commit

Permalink
feat: allow registration to be disabled (#2081)
Browse files Browse the repository at this point in the history
Closes #882
  • Loading branch information
nrutherford authored Dec 27, 2021
1 parent 790716e commit 864b00d
Show file tree
Hide file tree
Showing 13 changed files with 141 additions and 2 deletions.
5 changes: 5 additions & 0 deletions driver/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const (
ViperKeySelfServiceStrategyConfig = "selfservice.methods"
ViperKeySelfServiceBrowserDefaultReturnTo = "selfservice." + DefaultBrowserReturnURL
ViperKeyURLsWhitelistedReturnToDomains = "selfservice.whitelisted_return_urls"
ViperKeySelfServiceRegistrationEnabled = "selfservice.flows.registration.enabled"
ViperKeySelfServiceRegistrationUI = "selfservice.flows.registration.ui_url"
ViperKeySelfServiceRegistrationRequestLifespan = "selfservice.flows.registration.lifespan"
ViperKeySelfServiceRegistrationAfter = "selfservice.flows.registration.after"
Expand Down Expand Up @@ -514,6 +515,10 @@ func (p *Config) DisableAPIFlowEnforcement() bool {
return false
}

func (p *Config) SelfServiceFlowRegistrationEnabled() bool {
return p.p.Bool(ViperKeySelfServiceRegistrationEnabled)
}

func (p *Config) SelfServiceFlowVerificationEnabled() bool {
return p.p.Bool(ViperKeySelfServiceVerificationEnabled)
}
Expand Down
3 changes: 3 additions & 0 deletions driver/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ func TestViperProvider(t *testing.T) {
})

t.Run("method=registration", func(t *testing.T) {
assert.Equal(t, true, p.SelfServiceFlowRegistrationEnabled())
assert.Equal(t, time.Minute*98, p.SelfServiceFlowRegistrationRequestLifespan())

t.Run("hook=before", func(t *testing.T) {
Expand Down Expand Up @@ -516,6 +517,7 @@ func TestViperProvider_Defaults(t *testing.T) {
expect: func(t *testing.T, p *config.Config) {
assert.True(t, p.SelfServiceFlowRecoveryEnabled())
assert.False(t, p.SelfServiceFlowVerificationEnabled())
assert.True(t, p.SelfServiceFlowRegistrationEnabled())
assert.True(t, p.SelfServiceStrategy("password").Enabled)
assert.True(t, p.SelfServiceStrategy("profile").Enabled)
assert.True(t, p.SelfServiceStrategy("link").Enabled)
Expand All @@ -529,6 +531,7 @@ func TestViperProvider_Defaults(t *testing.T) {
expect: func(t *testing.T, p *config.Config) {
assert.False(t, p.SelfServiceFlowRecoveryEnabled())
assert.True(t, p.SelfServiceFlowVerificationEnabled())
assert.True(t, p.SelfServiceFlowRegistrationEnabled())
assert.True(t, p.SelfServiceStrategy("password").Enabled)
assert.True(t, p.SelfServiceStrategy("profile").Enabled)
assert.True(t, p.SelfServiceStrategy("link").Enabled)
Expand Down
1 change: 1 addition & 0 deletions driver/config/stub/.kratos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ selfservice:
body: /path/to/template.jsonnet

registration:
enabled: true
ui_url: http://test.kratos.ory.sh/register
lifespan: 98m
before:
Expand Down
6 changes: 6 additions & 0 deletions embedx/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,12 @@
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"title": "Enable User Registration",
"description": "If set to true will enable [User Registration](https://www.ory.sh/kratos/docs/self-service/flows/user-registration/).",
"default": true
},
"ui_url": {
"title": "Registration UI URL",
"description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).",
Expand Down
5 changes: 3 additions & 2 deletions selfservice/flow/registration/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import (
)

var (
ErrHookAbortFlow = errors.New("aborted registration hook execution")
ErrAlreadyLoggedIn = herodot.ErrBadRequest.WithID(text.ErrIDAlreadyLoggedIn).WithError("you are already logged in").WithReason("A valid session was detected and thus registration is not possible.")
ErrHookAbortFlow = errors.New("aborted registration hook execution")
ErrAlreadyLoggedIn = herodot.ErrBadRequest.WithID(text.ErrIDAlreadyLoggedIn).WithError("you are already logged in").WithReason("A valid session was detected and thus registration is not possible.")
ErrRegistrationDisabled = herodot.ErrBadRequest.WithID(text.ErrIDSelfServiceFlowDisabled).WithError("registration flow disabled").WithReason("Registration is not allowed because it was disabled.")
)

type (
Expand Down
2 changes: 2 additions & 0 deletions selfservice/flow/registration/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import (

func TestHandleError(t *testing.T) {
conf, reg := internal.NewFastRegistryWithMocks(t)

conf.MustSet(config.ViperKeySelfServiceRegistrationEnabled, true)
conf.MustSet(config.ViperKeyDefaultIdentitySchemaURL, "file://./stub/login.schema.json")

_, admin := testhelpers.NewKratosServer(t, reg)
Expand Down
11 changes: 11 additions & 0 deletions selfservice/flow/registration/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) {
}

func (h *Handler) NewRegistrationFlow(w http.ResponseWriter, r *http.Request, ft flow.Type) (*Flow, error) {

if !h.d.Config(r.Context()).SelfServiceFlowRegistrationEnabled() {
return nil, errors.WithStack(ErrRegistrationDisabled)
}

f, err := NewFlow(h.d.Config(r.Context()), h.d.Config(r.Context()).SelfServiceFlowRegistrationRequestLifespan(), h.d.GenerateCSRFToken(r), r, ft)
if err != nil {
return nil, err
Expand Down Expand Up @@ -299,6 +304,12 @@ type getSelfServiceRegistrationFlow struct {
// 410: jsonError
// 500: jsonError
func (h *Handler) fetchFlow(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {

if !h.d.Config(r.Context()).SelfServiceFlowRegistrationEnabled() {
h.d.SelfServiceErrorManager().Forward(r.Context(), w, r, errors.WithStack(ErrRegistrationDisabled))
return
}

ar, err := h.d.RegistrationFlowPersister().GetRegistrationFlow(r.Context(), x.ParseUUID(r.URL.Query().Get("id")))
if err != nil {
h.d.Writer().WriteError(w, r, err)
Expand Down
61 changes: 61 additions & 0 deletions selfservice/flow/registration/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func TestHandlerRedirectOnAuthenticated(t *testing.T) {
ts, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, x.NewRouterAdmin())

redirTS := testhelpers.NewRedirTS(t, "already authenticated", conf)
conf.MustSet(config.ViperKeySelfServiceRegistrationEnabled, true)
conf.MustSet(config.ViperKeyDefaultIdentitySchemaURL, "file://./stub/identity.schema.json")

t.Run("does redirect to default on authenticated request", func(t *testing.T) {
Expand All @@ -63,6 +64,7 @@ func TestInitFlow(t *testing.T) {
publicTS, _ := testhelpers.NewKratosServerWithRouters(t, reg, router, x.NewRouterAdmin())
registrationTS := testhelpers.NewRegistrationUIFlowEchoServer(t, reg)

conf.MustSet(config.ViperKeySelfServiceRegistrationEnabled, true)
conf.MustSet(config.ViperKeySelfServiceBrowserDefaultReturnTo, "https://www.ory.sh")
conf.MustSet(config.ViperKeyDefaultIdentitySchemaURL, "file://./stub/login.schema.json")

Expand Down Expand Up @@ -156,6 +158,7 @@ func TestInitFlow(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
assertx.EqualAsJSON(t, registration.ErrAlreadyLoggedIn, json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body)
})

t.Run("case=relative redirect when self-service registration ui is a relative URL", func(t *testing.T) {
reg.Config(context.Background()).MustSet(config.ViperKeySelfServiceRegistrationUI, "/registration-ts")
assert.Regexp(
Expand All @@ -167,8 +170,66 @@ func TestInitFlow(t *testing.T) {
})
}

func TestDisabledFlow(t *testing.T) {
conf, reg := internal.NewFastRegistryWithMocks(t)

conf.MustSet(config.ViperKeySelfServiceRegistrationEnabled, false)
conf.MustSet(config.ViperKeyDefaultIdentitySchemaURL, "file://./stub/login.schema.json")
conf.MustSet(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword),
map[string]interface{}{"enabled": true})

publicTS, _ := testhelpers.NewKratosServerWithCSRF(t, reg)
errTS := testhelpers.NewErrorTestServer(t, reg)

makeRequest := func(t *testing.T, route string, isSPA bool) (*http.Response, []byte) {
c := publicTS.Client()
req, err := http.NewRequest("GET", publicTS.URL+route, nil)
require.NoError(t, err)

if isSPA {
req.Header.Set("Accept", "application/json")
}

res, err := c.Do(req)
require.NoError(t, err)
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
return res, body
}

t.Run("flow=api", func(t *testing.T) {
t.Run("case=init fails when flow disabled", func(t *testing.T) {
res, body := makeRequest(t, registration.RouteInitAPIFlow, false)
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
assertx.EqualAsJSON(t, registration.ErrRegistrationDisabled, json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body)
})

t.Run("case=get flow fails when flow disabled", func(t *testing.T) {
res, body := makeRequest(t, registration.RouteGetFlow+"?id="+x.NewUUID().String(), false)
require.Contains(t, res.Request.URL.String(), errTS.URL, "%s", body)
assert.EqualValues(t, registration.ErrRegistrationDisabled.ReasonField, gjson.GetBytes(body, "reason").String(), "%s", body)
})
})

t.Run("flow=browser", func(t *testing.T) {
t.Run("case=init responds with error if flow disabled and SPA", func(t *testing.T) {
res, body := makeRequest(t, registration.RouteInitBrowserFlow, true)
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
assertx.EqualAsJSON(t, registration.ErrRegistrationDisabled, json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body)
})

t.Run("case=get flow responds with error if flow disabled and SPA", func(t *testing.T) {
res, body := makeRequest(t, registration.RouteGetFlow+"?id="+x.NewUUID().String(), true)
assert.Equal(t, http.StatusBadRequest, res.StatusCode)
assertx.EqualAsJSON(t, registration.ErrRegistrationDisabled, json.RawMessage(gjson.GetBytes(body, "error").Raw), "%s", body)
})
})
}

func TestGetFlow(t *testing.T) {
conf, reg := internal.NewFastRegistryWithMocks(t)
conf.MustSet(config.ViperKeySelfServiceRegistrationEnabled, true)
conf.MustSet(config.ViperKeyDefaultIdentitySchemaURL, "file://./stub/registration.schema.json")
conf.MustSet(config.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypePassword),
map[string]interface{}{"enabled": true})
Expand Down
2 changes: 2 additions & 0 deletions selfservice/strategy/oidc/strategy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ func TestStrategy(t *testing.T) {
Mapper: "file://./stub/oidc.hydra.jsonnet",
},
)

conf.MustSet(config.ViperKeySelfServiceRegistrationEnabled, true)
conf.MustSet(config.ViperKeyDefaultIdentitySchemaURL, "file://./stub/registration.schema.json")
conf.MustSet(config.HookStrategyKey(config.ViperKeySelfServiceRegistrationAfter,
identity.CredentialsTypeOIDC.String()), []config.SelfServiceHook{{Name: "session"}})
Expand Down
22 changes: 22 additions & 0 deletions test/e2e/cypress/integration/profiles/oidc/login/error.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,28 @@ context('Social Sign In Errors', () => {
'no id_token'
)
})

it('should fail to convert a sign in flow to a sign up flow when registration is disabled', () => {
cy.disableRegistration()

const email = gen.email()
cy.visit(login)
cy.triggerOidc()

cy.get('#username').clear().type(email)
cy.get('#remember').click()
cy.get('#accept').click()
cy.get('[name="scope"]').each(($el) => cy.wrap($el).click())
cy.get('#remember').click()
cy.get('#accept').click()

cy.get('[data-testid="ui/message/4000001"]').should(
'contain.text',
'Registration is not allowed because it was disabled'
)

cy.noSession()
})
})
})
})
14 changes: 14 additions & 0 deletions test/e2e/cypress/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,20 @@ Cypress.Commands.add('disableRecovery', ({} = {}) => {
})
})

Cypress.Commands.add('disableRegistration', ({} = {}) => {
updateConfigFile((config) => {
config.selfservice.flows.registration.enabled = false
return config
})
})

Cypress.Commands.add('enableRegistration', ({} = {}) => {
updateConfigFile((config) => {
config.selfservice.flows.registration.enabled = true
return config
})
})

Cypress.Commands.add('useLaxAal', ({} = {}) => {
updateConfigFile((config) => {
config.selfservice.flows.settings.required_aal = 'aal1'
Expand Down
10 changes: 10 additions & 0 deletions test/e2e/cypress/support/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,16 @@ declare global {
*/
disableRecovery(): Chainable<void>

/**
* Disables registration
*/
disableRegistration(): Chainable<void>

/**
* Enables registration
*/
enableRegistration(): Chainable<void>

/**
* Expect a recovery email which is valid.
*
Expand Down
1 change: 1 addition & 0 deletions text/message_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package text
const (
ErrIDNeedsPrivilegedSession = "session_refresh_required"
ErrIDSelfServiceFlowExpired = "self_service_flow_expired"
ErrIDSelfServiceFlowDisabled = "self_service_flow_disabled"
ErrIDSelfServiceBrowserLocationChangeRequiredError = "browser_location_change_required"

ErrIDAlreadyLoggedIn = "session_already_available"
Expand Down

0 comments on commit 864b00d

Please sign in to comment.