Skip to content
Open
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
12 changes: 12 additions & 0 deletions pkg/lib/authenticationflow/declarative/generate_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ const (
nameStepReauthenticate = "reauthenticate"
)

// GenerateAuthenticationFlowConfig returns the flow of each type that Authgear generates from cfg.
func GenerateAuthenticationFlowConfig(cfg *config.AppConfig) *config.AuthenticationFlowConfig {
return &config.AuthenticationFlowConfig{
SignupFlows: []*config.AuthenticationFlowSignupFlow{GenerateSignupFlowConfig(cfg)},
PromoteFlows: []*config.AuthenticationFlowSignupFlow{GeneratePromoteFlowConfig(cfg)},
LoginFlows: []*config.AuthenticationFlowLoginFlow{GenerateLoginFlowConfig(cfg)},
SignupLoginFlows: []*config.AuthenticationFlowSignupLoginFlow{GenerateSignupLoginFlowConfig(cfg)},
ReauthFlows: []*config.AuthenticationFlowReauthFlow{GenerateReauthFlowConfig(cfg)},
AccountRecoveryFlows: []*config.AuthenticationFlowAccountRecoveryFlow{GenerateAccountRecoveryFlowConfig(cfg)},
}
}

// nameStepIdentify returns a name that is unique across flow types.
// In account linking, if the same is used in signup flow, and login flow,
// then in type: verify, the type: identify in the login flow is found,
Expand Down
129 changes: 129 additions & 0 deletions pkg/lib/authenticationflow/declarative/generate_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package declarative

import (
"context"
"encoding/json"
"testing"

. "github.com/smartystreets/goconvey/convey"
"sigs.k8s.io/yaml"

"github.com/authgear/authgear-server/pkg/lib/config"
_ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/google"
)

func TestGenerateAuthenticationFlowConfig(t *testing.T) {
Convey("GenerateAuthenticationFlowConfig", t, func() {
ctx := context.Background()

// Saving the generated flows unchanged as custom flows must produce a valid config,
// except where expectedErr says otherwise.
test := func(cfgYAML string, expectedErr string) {
appConfig, err := config.Parse(ctx, []byte(cfgYAML))
So(err, ShouldBeNil)

flows := GenerateAuthenticationFlowConfig(appConfig)
So(flows.SignupFlows, ShouldHaveLength, 1)
So(flows.PromoteFlows, ShouldHaveLength, 1)
So(flows.LoginFlows, ShouldHaveLength, 1)
So(flows.SignupLoginFlows, ShouldHaveLength, 1)
So(flows.ReauthFlows, ShouldHaveLength, 1)
So(flows.AccountRecoveryFlows, ShouldHaveLength, 1)

flowsJSON, err := json.Marshal(flows)
So(err, ShouldBeNil)
var flowsMap map[string]any
err = json.Unmarshal(flowsJSON, &flowsMap)
So(err, ShouldBeNil)

var raw map[string]any
err = yaml.Unmarshal([]byte(cfgYAML), &raw)
So(err, ShouldBeNil)
raw["authentication_flow"] = flowsMap

roundTripped, err := yaml.Marshal(raw)
So(err, ShouldBeNil)
_, err = config.Parse(ctx, roundTripped)
if expectedErr == "" {
So(err, ShouldBeNil)
} else {
So(err, ShouldBeError, expectedErr)
}
}

Convey("default settings", func() {
test(`
id: test
http:
public_origin: http://test
`, "")
})

Convey("phone with SMS OTP", func() {
test(`
id: test
http:
public_origin: http://test
authentication:
identities:
- login_id
primary_authenticators:
- oob_otp_sms
identity:
login_id:
keys:
- type: phone
`, "")
})

// Without an email or phone login ID there is no way to recover an account,
// so the generated account recovery flow has no options.
Convey("username and password with required 2FA", func() {
test(`
id: test
http:
public_origin: http://test
authentication:
identities:
- login_id
primary_authenticators:
- password
secondary_authenticators:
- totp
- oob_otp_email
secondary_authentication_mode: required
identity:
login_id:
keys:
- type: username
`, `invalid configuration:
/authentication_flow/account_recovery_flows/0/steps/0: required
map[actual:[type] expected:[one_of] missing:[one_of]]`)
})

Convey("email with passkey and an OAuth provider", func() {
test(`
id: test
http:
public_origin: http://test
authentication:
identities:
- login_id
- oauth
- passkey
primary_authenticators:
- password
- passkey
identity:
login_id:
keys:
- type: email
oauth:
providers:
- alias: google
type: google
client_id: client-id
`, "")
})
})
}
17 changes: 17 additions & 0 deletions pkg/lib/config/authentication_flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,23 @@ var _ = Schema.Add("AuthenticationFlowSignupLoginFlowIdentify", `
"then": {
"required": ["login_flow"],
"not": { "required": ["signup_flow"] }
}
},
{
"if": {
"properties": {
"identification": { "const": "passkey" }
}
},
"then": {
"required": ["login_flow"]
}
},
{
"if": {
"properties": {
"identification": { "enum": ["select_account", "passkey"] }
}
},
"else": {
"required": ["signup_flow", "login_flow"]
Expand Down
36 changes: 36 additions & 0 deletions pkg/lib/config/testdata/signup_login_flow_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,39 @@ value:
- type: identify
one_of:
- identification: select_account
---
part: AuthenticationFlowSignupLoginFlow
name: passkey-without-signup-flow
error: null
value:
name: id
steps:
- type: identify
one_of:
- identification: passkey
login_flow: b
---
part: AuthenticationFlowSignupLoginFlow
name: passkey-with-signup-flow
error: null
value:
name: id
steps:
- type: identify
one_of:
- identification: passkey
signup_flow: a
login_flow: b
---
part: AuthenticationFlowSignupLoginFlow
name: passkey-without-login-flow-is-invalid
error: |-
invalid value:
/steps/0/one_of/0: required
map[actual:[identification] expected:[login_flow] missing:[login_flow]]
value:
name: id
steps:
- type: identify
one_of:
- identification: passkey
8 changes: 8 additions & 0 deletions pkg/portal/graphql/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

relay "github.com/authgear/authgear-server/pkg/graphqlgo/relay"

"github.com/authgear/authgear-server/pkg/lib/authenticationflow/declarative"
"github.com/authgear/authgear-server/pkg/lib/config"
"github.com/authgear/authgear-server/pkg/lib/config/configsource"
"github.com/authgear/authgear-server/pkg/portal/model"
Expand Down Expand Up @@ -535,6 +536,13 @@ var nodeApp = node(
return model.NewPortalFeatureConfig(p.Source.(*model.App).Context.Config.FeatureConfig), nil
},
},
"generatedAuthenticationFlows": &graphql.Field{
Description: "The authentication flows generated from the app's settings. A custom flow named \"default\" replaces the generated flow of its type.",
Type: graphql.NewNonNull(AuthenticationFlowConfig),
Resolve: func(p graphql.ResolveParams) (any, error) {
return declarative.GenerateAuthenticationFlowConfig(p.Source.(*model.App).Context.Config.AppConfig), nil
},
},
"usage": &graphql.Field{
Type: usage,
Args: graphql.FieldConfigArgument{
Expand Down
5 changes: 5 additions & 0 deletions pkg/portal/graphql/scalars.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ var FeatureConfig = graphqlutil.NewJSONObjectScalar(
"The `FeatureConfig` scalar type represents an feature config JSON object",
)

var AuthenticationFlowConfig = graphqlutil.NewJSONObjectScalar(
"AuthenticationFlowConfig",
"The `AuthenticationFlowConfig` scalar type represents the authentication_flow section of an app config",
)

var TutorialStatusData = graphqlutil.NewJSONObjectScalar(
"TutorialStatusData",
"The `TutorialStatusData` scalar type represents tutorial status data",
Expand Down
4 changes: 4 additions & 0 deletions portal/src/graphql/portal/globalTypes.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export type Scalars = {
Float: { input: number; output: number; }
/** The `AppConfig` scalar type represents an app config JSON object */
AppConfig: { input: GQL_AppConfig; output: GQL_AppConfig; }
/** The `AuthenticationFlowConfig` scalar type represents the authentication_flow section of an app config */
AuthenticationFlowConfig: { input: any; output: any; }
/** The `Date` scalar type represents a Date. The Date is serialized in ISO 8601 format */
Date: { input: GQL_Date; output: GQL_Date; }
/** The `DateTime` scalar type represents a DateTime. The DateTime is serialized as an RFC 3339 quoted string */
Expand Down Expand Up @@ -65,6 +67,8 @@ export type App = Node & {
effectiveAppConfig: Scalars['AppConfig']['output'];
effectiveFeatureConfig: Scalars['FeatureConfig']['output'];
effectiveSecretConfig: EffectiveSecretConfig;
/** The authentication flows generated from the app's settings. A custom flow named "default" replaces the generated flow of its type. */
generatedAuthenticationFlows: Scalars['AuthenticationFlowConfig']['output'];
/** The ID of an object */
id: Scalars['ID']['output'];
isProcessingSubscription: Scalars['Boolean']['output'];
Expand Down
10 changes: 10 additions & 0 deletions portal/src/graphql/portal/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ type App implements Node {
""""""
effectiveSecretConfig: EffectiveSecretConfig!

"""
The authentication flows generated from the app's settings. A custom flow named "default" replaces the generated flow of its type.
"""
generatedAuthenticationFlows: AuthenticationFlowConfig!

"""The ID of an object"""
id: ID!

Expand Down Expand Up @@ -185,6 +190,11 @@ enum AppSecretKey {
WEBHOOK_SECRET
}

"""
The `AuthenticationFlowConfig` scalar type represents the authentication_flow section of an app config
"""
scalar AuthenticationFlowConfig

"""Bot protection provider secret"""
type BotProtectionProviderSecret {
""""""
Expand Down
Loading