diff --git a/Makefile b/Makefile index 553371d7bb..5745274b3c 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,7 @@ vendor: go install github.com/golang/mock/mockgen go install github.com/google/wire/cmd/wire go install golang.org/x/vuln/cmd/govulncheck@latest + go install golang.org/x/tools/cmd/goimports@latest npm --prefix ./scripts/npm ci npm --prefix ./authui ci npm --prefix ./portal ci @@ -57,7 +58,8 @@ lint: .PHONY: fmt fmt: - go fmt ./... + # Ignore generated files, such as wire_gen.go and *_mock_test.go + find ./pkg ./cmd ./e2e -name '*.go' -not -name 'wire_gen.go' -not -name '*_mock_test.go' | sort | xargs goimports -w -format-only -local github.com/authgear/authgear-server .PHONY: govulncheck govulncheck: diff --git a/cmd/authgear/background/wire.go b/cmd/authgear/background/wire.go index 718d612d4e..c45a324185 100644 --- a/cmd/authgear/background/wire.go +++ b/cmd/authgear/background/wire.go @@ -5,6 +5,7 @@ package background import ( "context" + "github.com/google/wire" "github.com/authgear/authgear-server/pkg/lib/config" diff --git a/cmd/authgear/main.go b/cmd/authgear/main.go index 66beda7014..7baffbaf1f 100644 --- a/cmd/authgear/main.go +++ b/cmd/authgear/main.go @@ -21,6 +21,15 @@ import ( _ "github.com/authgear/authgear-server/cmd/authgear/cmd/cmdstart" _ "github.com/authgear/authgear-server/pkg/latte" _ "github.com/authgear/authgear-server/pkg/lib/authenticationflow/declarative" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/adfs" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/apple" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/azureadb2c" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/azureadv2" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/facebook" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/github" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/google" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/linkedin" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/wechat" "github.com/authgear/authgear-server/pkg/util/debug" ) diff --git a/cmd/portal/analytic/wire.go b/cmd/portal/analytic/wire.go index b140aa3083..66c9a6ff28 100644 --- a/cmd/portal/analytic/wire.go +++ b/cmd/portal/analytic/wire.go @@ -6,13 +6,14 @@ package analytic import ( "context" + "github.com/google/wire" + "github.com/authgear/authgear-server/pkg/lib/analytic" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/infra/db" "github.com/authgear/authgear-server/pkg/lib/infra/redis" "github.com/authgear/authgear-server/pkg/util/clock" "github.com/authgear/authgear-server/pkg/util/periodical" - "github.com/google/wire" ) func NewUserWeeklyReport( diff --git a/cmd/portal/cmd/cmdanalytic/posthog.go b/cmd/portal/cmd/cmdanalytic/posthog.go index 8d05cf521f..19033c8736 100644 --- a/cmd/portal/cmd/cmdanalytic/posthog.go +++ b/cmd/portal/cmd/cmdanalytic/posthog.go @@ -2,6 +2,7 @@ package cmdanalytic import ( "context" + "github.com/spf13/cobra" "github.com/authgear/authgear-server/cmd/portal/analytic" diff --git a/cmd/portal/cmd/cmdinternal/migrate_resources_remove_is_first_party.go b/cmd/portal/cmd/cmdinternal/migrate_resources_remove_is_first_party.go index 48b2687862..9df7a6b61f 100644 --- a/cmd/portal/cmd/cmdinternal/migrate_resources_remove_is_first_party.go +++ b/cmd/portal/cmd/cmdinternal/migrate_resources_remove_is_first_party.go @@ -5,10 +5,11 @@ import ( "fmt" "log" - portalcmd "github.com/authgear/authgear-server/cmd/portal/cmd" - "github.com/authgear/authgear-server/cmd/portal/internal" "github.com/spf13/cobra" "sigs.k8s.io/yaml" + + portalcmd "github.com/authgear/authgear-server/cmd/portal/cmd" + "github.com/authgear/authgear-server/cmd/portal/internal" ) var cmdInternalMigrateRemoveIsFirstParty = &cobra.Command{ diff --git a/cmd/portal/main.go b/cmd/portal/main.go index 2b6b4a702f..e87256de90 100644 --- a/cmd/portal/main.go +++ b/cmd/portal/main.go @@ -16,6 +16,15 @@ import ( _ "github.com/authgear/authgear-server/cmd/portal/cmd/cmdpricing" _ "github.com/authgear/authgear-server/cmd/portal/cmd/cmdstart" _ "github.com/authgear/authgear-server/cmd/portal/cmd/cmdusage" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/adfs" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/apple" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/azureadb2c" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/azureadv2" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/facebook" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/github" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/google" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/linkedin" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/wechat" "github.com/authgear/authgear-server/pkg/util/debug" ) diff --git a/cmd/portal/server/wire.go b/cmd/portal/server/wire.go index dd6aa56774..7ac202d722 100644 --- a/cmd/portal/server/wire.go +++ b/cmd/portal/server/wire.go @@ -5,6 +5,7 @@ package server import ( "context" + "github.com/google/wire" "github.com/authgear/authgear-server/pkg/lib/config" diff --git a/cmd/portal/usage/deps.go b/cmd/portal/usage/deps.go index d6826fcb31..e985c9bfb4 100644 --- a/cmd/portal/usage/deps.go +++ b/cmd/portal/usage/deps.go @@ -1,6 +1,8 @@ package usage import ( + "github.com/google/wire" + "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/infra/db/auditdb" "github.com/authgear/authgear-server/pkg/lib/infra/db/globaldb" @@ -8,7 +10,6 @@ import ( "github.com/authgear/authgear-server/pkg/lib/meter" "github.com/authgear/authgear-server/pkg/lib/usage" "github.com/authgear/authgear-server/pkg/util/cobrasentry" - "github.com/google/wire" ) func NewGlobalDatabaseCredentials(dbCredentials *config.DatabaseCredentials) *config.GlobalDatabaseCredentialsEnvironmentConfig { diff --git a/cmd/portal/usage/wire.go b/cmd/portal/usage/wire.go index 49598fc712..74d32b797c 100644 --- a/cmd/portal/usage/wire.go +++ b/cmd/portal/usage/wire.go @@ -8,11 +8,12 @@ import ( "github.com/getsentry/sentry-go" + "github.com/google/wire" + "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/infra/db" "github.com/authgear/authgear-server/pkg/lib/infra/redis" "github.com/authgear/authgear-server/pkg/lib/usage" - "github.com/google/wire" ) func NewCountCollector( diff --git a/e2e/cmd/e2e/cmd/configsource.go b/e2e/cmd/e2e/cmd/configsource.go index d816e6906e..30ad995fe7 100644 --- a/e2e/cmd/e2e/cmd/configsource.go +++ b/e2e/cmd/e2e/cmd/configsource.go @@ -1,8 +1,9 @@ package cmd import ( - e2e "github.com/authgear/authgear-server/e2e/cmd/e2e/pkg" "github.com/spf13/cobra" + + e2e "github.com/authgear/authgear-server/e2e/cmd/e2e/pkg" ) func init() { diff --git a/e2e/cmd/e2e/cmd/execsql.go b/e2e/cmd/e2e/cmd/execsql.go index c0ad81f03b..0dfe3d4d59 100644 --- a/e2e/cmd/e2e/cmd/execsql.go +++ b/e2e/cmd/e2e/cmd/execsql.go @@ -1,8 +1,9 @@ package cmd import ( - e2e "github.com/authgear/authgear-server/e2e/cmd/e2e/pkg" "github.com/spf13/cobra" + + e2e "github.com/authgear/authgear-server/e2e/cmd/e2e/pkg" ) func init() { diff --git a/e2e/cmd/e2e/cmd/otp.go b/e2e/cmd/e2e/cmd/otp.go index 75bff289b7..c420b16856 100644 --- a/e2e/cmd/e2e/cmd/otp.go +++ b/e2e/cmd/e2e/cmd/otp.go @@ -3,8 +3,9 @@ package cmd import ( "fmt" - e2e "github.com/authgear/authgear-server/e2e/cmd/e2e/pkg" "github.com/spf13/cobra" + + e2e "github.com/authgear/authgear-server/e2e/cmd/e2e/pkg" ) func init() { diff --git a/e2e/cmd/e2e/cmd/userimport.go b/e2e/cmd/e2e/cmd/userimport.go index dc93884111..d106192ecb 100644 --- a/e2e/cmd/e2e/cmd/userimport.go +++ b/e2e/cmd/e2e/cmd/userimport.go @@ -3,8 +3,9 @@ package cmd import ( "os" - e2e "github.com/authgear/authgear-server/e2e/cmd/e2e/pkg" "github.com/spf13/cobra" + + e2e "github.com/authgear/authgear-server/e2e/cmd/e2e/pkg" ) func init() { diff --git a/e2e/cmd/e2e/main.go b/e2e/cmd/e2e/main.go index 382f4bb64d..df7ee5a12a 100644 --- a/e2e/cmd/e2e/main.go +++ b/e2e/cmd/e2e/main.go @@ -5,10 +5,20 @@ import ( "log" "os" - cmd "github.com/authgear/authgear-server/e2e/cmd/e2e/cmd" - "github.com/authgear/authgear-server/pkg/util/debug" "github.com/joho/godotenv" _ "go.uber.org/automaxprocs" + + cmd "github.com/authgear/authgear-server/e2e/cmd/e2e/cmd" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/adfs" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/apple" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/azureadb2c" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/azureadv2" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/facebook" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/github" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/google" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/linkedin" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/wechat" + "github.com/authgear/authgear-server/pkg/util/debug" ) func main() { diff --git a/e2e/cmd/e2e/pkg/deps.go b/e2e/cmd/e2e/pkg/deps.go index 13a125319b..b9511498b3 100644 --- a/e2e/cmd/e2e/pkg/deps.go +++ b/e2e/cmd/e2e/pkg/deps.go @@ -3,9 +3,10 @@ package e2e import ( "net/http" + "github.com/google/wire" + deps "github.com/authgear/authgear-server/pkg/lib/deps" "github.com/authgear/authgear-server/pkg/util/httputil" - "github.com/google/wire" ) func ProvideEnd2EndHTTPRequest() *http.Request { diff --git a/e2e/cmd/proxy/main.go b/e2e/cmd/proxy/main.go index a74a18e209..ef184d7610 100644 --- a/e2e/cmd/proxy/main.go +++ b/e2e/cmd/proxy/main.go @@ -11,11 +11,12 @@ import ( "os/signal" "time" - "github.com/authgear/authgear-server/e2e/cmd/proxy/mockoidc" - "github.com/authgear/authgear-server/e2e/cmd/proxy/modifier" "github.com/google/martian" "github.com/google/martian/httpspec" "github.com/google/martian/mitm" + + "github.com/authgear/authgear-server/e2e/cmd/proxy/mockoidc" + "github.com/authgear/authgear-server/e2e/cmd/proxy/modifier" ) func main() { diff --git a/e2e/cmd/proxy/modifier/oidc.go b/e2e/cmd/proxy/modifier/oidc.go index b52bcd8c5e..3e1ab963a1 100644 --- a/e2e/cmd/proxy/modifier/oidc.go +++ b/e2e/cmd/proxy/modifier/oidc.go @@ -4,8 +4,9 @@ import ( "net/http" "net/url" - "github.com/authgear/authgear-server/e2e/cmd/proxy/mockoidc" "github.com/google/martian/parse" + + "github.com/authgear/authgear-server/e2e/cmd/proxy/mockoidc" ) func init() { diff --git a/e2e/pkg/testrunner/testcase.go b/e2e/pkg/testrunner/testcase.go index 057ac352db..f4adf98543 100644 --- a/e2e/pkg/testrunner/testcase.go +++ b/e2e/pkg/testrunner/testcase.go @@ -12,6 +12,7 @@ import ( texttemplate "text/template" "github.com/Masterminds/sprig" + authflowclient "github.com/authgear/authgear-server/e2e/pkg/e2eclient" "github.com/authgear/authgear-server/pkg/util/httputil" ) diff --git a/go.mod b/go.mod index 478a03c6a2..d2ec3c4ec2 100644 --- a/go.mod +++ b/go.mod @@ -94,7 +94,10 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require github.com/go-jose/go-jose/v3 v3.0.3 +require ( + github.com/authgear/oauthrelyingparty v1.0.0 + github.com/go-jose/go-jose/v3 v3.0.3 +) require ( cloud.google.com/go v0.110.8 // indirect diff --git a/go.sum b/go.sum index 15ab7e02a3..0cb1eb0577 100644 --- a/go.sum +++ b/go.sum @@ -87,6 +87,8 @@ github.com/alicebob/miniredis/v2 v2.31.0 h1:ObEFUNlJwoIiyjxdrYF0QIDE7qXcLc7D3WpS github.com/alicebob/miniredis/v2 v2.31.0/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg= github.com/authgear/graphql-go-relay v0.0.0-20201016065100-df672205b892 h1:OIPk6DEk51wL35vsCYfLacc7pZ0ev8TKvmcp+EKOnPU= github.com/authgear/graphql-go-relay v0.0.0-20201016065100-df672205b892/go.mod h1:SxJGRjo7+1SFr8pfMghiCM17WWFSswRwMvtv6t1aDO8= +github.com/authgear/oauthrelyingparty v1.0.0 h1:i5F3mjnImWesbdOc7ZcjPcT5918FAwruduRmFTBMtdY= +github.com/authgear/oauthrelyingparty v1.0.0/go.mod h1:0UcO0p5eS9BPLulX6K7TvkXkuPeTn3QhAJ/UQg4fmiQ= github.com/aws/aws-sdk-go v1.47.9 h1:rarTsos0mA16q+huicGx0e560aYRtOucV5z2Mw23JRY= github.com/aws/aws-sdk-go v1.47.9/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= diff --git a/pkg/admin/graphql/group_mutation.go b/pkg/admin/graphql/group_mutation.go index 24fda28c4f..bbff0a09b9 100644 --- a/pkg/admin/graphql/group_mutation.go +++ b/pkg/admin/graphql/group_mutation.go @@ -3,13 +3,14 @@ package graphql import ( relay "github.com/authgear/graphql-go-relay" + "github.com/graphql-go/graphql" + "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/api/event/nonblocking" "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/rolesgroups" "github.com/authgear/authgear-server/pkg/util/graphqlutil" "github.com/authgear/authgear-server/pkg/util/slice" - "github.com/graphql-go/graphql" ) var createGroupInput = graphql.NewInputObject(graphql.InputObjectConfig{ diff --git a/pkg/admin/graphql/group_role_mutation.go b/pkg/admin/graphql/group_role_mutation.go index 2805fc205a..c39a466184 100644 --- a/pkg/admin/graphql/group_role_mutation.go +++ b/pkg/admin/graphql/group_role_mutation.go @@ -1,12 +1,13 @@ package graphql import ( + "github.com/graphql-go/graphql" + "github.com/authgear/authgear-server/pkg/api/event/nonblocking" "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/rolesgroups" "github.com/authgear/authgear-server/pkg/util/graphqlutil" "github.com/authgear/authgear-server/pkg/util/slice" - "github.com/graphql-go/graphql" ) var addRoleToGroupsInput = graphql.NewInputObject(graphql.InputObjectConfig{ diff --git a/pkg/admin/graphql/group_user_mutation.go b/pkg/admin/graphql/group_user_mutation.go index e5c64bd655..4cfe5714a3 100644 --- a/pkg/admin/graphql/group_user_mutation.go +++ b/pkg/admin/graphql/group_user_mutation.go @@ -1,14 +1,15 @@ package graphql import ( + relay "github.com/authgear/graphql-go-relay" + "github.com/graphql-go/graphql" + "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/api/event/nonblocking" "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/rolesgroups" "github.com/authgear/authgear-server/pkg/util/graphqlutil" "github.com/authgear/authgear-server/pkg/util/slice" - relay "github.com/authgear/graphql-go-relay" - "github.com/graphql-go/graphql" ) var addGroupToUsersInput = graphql.NewInputObject(graphql.InputObjectConfig{ diff --git a/pkg/admin/graphql/role_mutation.go b/pkg/admin/graphql/role_mutation.go index 305be6ebea..4d45ec94b4 100644 --- a/pkg/admin/graphql/role_mutation.go +++ b/pkg/admin/graphql/role_mutation.go @@ -3,13 +3,14 @@ package graphql import ( relay "github.com/authgear/graphql-go-relay" + "github.com/graphql-go/graphql" + "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/api/event/nonblocking" "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/rolesgroups" "github.com/authgear/authgear-server/pkg/util/graphqlutil" "github.com/authgear/authgear-server/pkg/util/slice" - "github.com/graphql-go/graphql" ) var createRoleInput = graphql.NewInputObject(graphql.InputObjectConfig{ diff --git a/pkg/admin/graphql/role_user_mutation.go b/pkg/admin/graphql/role_user_mutation.go index 8ef71d5f40..bf4808eec4 100644 --- a/pkg/admin/graphql/role_user_mutation.go +++ b/pkg/admin/graphql/role_user_mutation.go @@ -1,14 +1,15 @@ package graphql import ( + relay "github.com/authgear/graphql-go-relay" + "github.com/graphql-go/graphql" + "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/api/event/nonblocking" "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/rolesgroups" "github.com/authgear/authgear-server/pkg/util/graphqlutil" "github.com/authgear/authgear-server/pkg/util/slice" - relay "github.com/authgear/graphql-go-relay" - "github.com/graphql-go/graphql" ) var addRoleToUsersInput = graphql.NewInputObject(graphql.InputObjectConfig{ diff --git a/pkg/api/internalinterface/loginid_normalizer.go b/pkg/api/internalinterface/loginid_normalizer.go new file mode 100644 index 0000000000..070f231d45 --- /dev/null +++ b/pkg/api/internalinterface/loginid_normalizer.go @@ -0,0 +1,6 @@ +package internalinterface + +type LoginIDNormalizer interface { + Normalize(loginID string) (string, error) + ComputeUniqueKey(normalizeLoginID string) (string, error) +} diff --git a/pkg/api/model/siwe.go b/pkg/api/model/siwe.go index 178f6d6258..348be45bae 100644 --- a/pkg/api/model/siwe.go +++ b/pkg/api/model/siwe.go @@ -5,8 +5,9 @@ import ( "encoding/hex" "fmt" - "github.com/authgear/authgear-server/pkg/util/web3" "github.com/ethereum/go-ethereum/crypto" + + "github.com/authgear/authgear-server/pkg/util/web3" ) type SIWEPublicKey string diff --git a/pkg/api/model/siwe_test.go b/pkg/api/model/siwe_test.go index 2f4d69a9f0..e9d2816734 100644 --- a/pkg/api/model/siwe_test.go +++ b/pkg/api/model/siwe_test.go @@ -6,9 +6,10 @@ import ( "crypto/rand" "testing" - "github.com/authgear/authgear-server/pkg/api/model" . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/authgear-server/pkg/api/model" + "github.com/ethereum/go-ethereum/crypto" ) diff --git a/pkg/auth/handler/api/authenticationflow_v1_create.go b/pkg/auth/handler/api/authenticationflow_v1_create.go index 4dcf07da20..e75a0eb328 100644 --- a/pkg/auth/handler/api/authenticationflow_v1_create.go +++ b/pkg/auth/handler/api/authenticationflow_v1_create.go @@ -6,6 +6,8 @@ import ( "net/http" "net/url" + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/authgear-server/pkg/api" authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" "github.com/authgear/authgear-server/pkg/lib/infra/redis/appredis" @@ -17,7 +19,6 @@ import ( "github.com/authgear/authgear-server/pkg/util/log" "github.com/authgear/authgear-server/pkg/util/slice" "github.com/authgear/authgear-server/pkg/util/validation" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) func ConfigureAuthenticationFlowV1CreateRoute(route httproute.Route) httproute.Route { diff --git a/pkg/auth/handler/webapp/authflow_controller.go b/pkg/auth/handler/webapp/authflow_controller.go index 31066f311a..38ca187b8d 100644 --- a/pkg/auth/handler/webapp/authflow_controller.go +++ b/pkg/auth/handler/webapp/authflow_controller.go @@ -8,6 +8,8 @@ import ( "net/url" "strconv" + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/authgear-server/pkg/api" "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/api/model" @@ -24,7 +26,6 @@ import ( "github.com/authgear/authgear-server/pkg/util/httputil" "github.com/authgear/authgear-server/pkg/util/log" "github.com/authgear/authgear-server/pkg/util/setutil" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) //go:generate mockgen -source=authflow_controller.go -destination=authflow_controller_mock_test.go -package webapp diff --git a/pkg/auth/handler/webapp/authflow_login.go b/pkg/auth/handler/webapp/authflow_login.go index 600ca93791..b6523d4937 100644 --- a/pkg/auth/handler/webapp/authflow_login.go +++ b/pkg/auth/handler/webapp/authflow_login.go @@ -6,10 +6,11 @@ import ( "net/http" "net/url" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/auth/handler/webapp/viewmodels" "github.com/authgear/authgear-server/pkg/auth/webapp" authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/meter" "github.com/authgear/authgear-server/pkg/util/httproute" "github.com/authgear/authgear-server/pkg/util/httputil" @@ -88,7 +89,7 @@ func (h *AuthflowLoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) "identification": "oauth", "alias": providerAlias, "redirect_uri": callbackURL, - "response_mode": string(sso.ResponseModeFormPost), + "response_mode": oauthrelyingparty.ResponseModeFormPost, } result, err := h.Controller.ReplaceScreen(r, s, authflow.FlowTypeSignupLogin, input) diff --git a/pkg/auth/handler/webapp/authflow_promote.go b/pkg/auth/handler/webapp/authflow_promote.go index 52b0b994fd..64f2dcd16d 100644 --- a/pkg/auth/handler/webapp/authflow_promote.go +++ b/pkg/auth/handler/webapp/authflow_promote.go @@ -4,10 +4,11 @@ import ( "net/http" "net/url" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/auth/handler/webapp/viewmodels" "github.com/authgear/authgear-server/pkg/auth/webapp" authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/util/httproute" "github.com/authgear/authgear-server/pkg/util/template" "github.com/authgear/authgear-server/pkg/util/validation" @@ -82,7 +83,7 @@ func (h *AuthflowPromoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques "identification": "oauth", "alias": providerAlias, "redirect_uri": callbackURL, - "response_mode": string(sso.ResponseModeFormPost), + "response_mode": oauthrelyingparty.ResponseModeFormPost, } result, err := h.Controller.AdvanceWithInput(r, s, screen, input, nil) diff --git a/pkg/auth/handler/webapp/authflow_signup.go b/pkg/auth/handler/webapp/authflow_signup.go index 6e84a40bd3..4f4dcf820f 100644 --- a/pkg/auth/handler/webapp/authflow_signup.go +++ b/pkg/auth/handler/webapp/authflow_signup.go @@ -5,10 +5,11 @@ import ( "net/http" "net/url" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/auth/handler/webapp/viewmodels" "github.com/authgear/authgear-server/pkg/auth/webapp" authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/meter" "github.com/authgear/authgear-server/pkg/util/httproute" "github.com/authgear/authgear-server/pkg/util/httputil" @@ -98,7 +99,7 @@ func (h *AuthflowSignupHandler) ServeHTTP(w http.ResponseWriter, r *http.Request "identification": "oauth", "alias": providerAlias, "redirect_uri": callbackURL, - "response_mode": string(sso.ResponseModeFormPost), + "response_mode": oauthrelyingparty.ResponseModeFormPost, } result, err := h.Controller.ReplaceScreen(r, s, authflow.FlowTypeSignupLogin, input) diff --git a/pkg/auth/handler/webapp/authflowv2/account_linking.go b/pkg/auth/handler/webapp/authflowv2/account_linking.go index 484212bb7c..e7f939d369 100644 --- a/pkg/auth/handler/webapp/authflowv2/account_linking.go +++ b/pkg/auth/handler/webapp/authflowv2/account_linking.go @@ -39,7 +39,7 @@ func ConfigureAuthflowV2AccountLinkingRoute(route httproute.Route) httproute.Rou type AuthflowV2AccountLinkingOption struct { Identification config.AuthenticationFlowIdentification MaskedDisplayName string - ProviderType config.OAuthSSOProviderType + ProviderType string Index int } diff --git a/pkg/auth/handler/webapp/authflowv2/internal_signup_login.go b/pkg/auth/handler/webapp/authflowv2/internal_signup_login.go index 9e947e03ed..6c92fd168a 100644 --- a/pkg/auth/handler/webapp/authflowv2/internal_signup_login.go +++ b/pkg/auth/handler/webapp/authflowv2/internal_signup_login.go @@ -5,12 +5,13 @@ import ( "fmt" "net/http" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + handlerwebapp "github.com/authgear/authgear-server/pkg/auth/handler/webapp" v2viewmodels "github.com/authgear/authgear-server/pkg/auth/handler/webapp/authflowv2/viewmodels" "github.com/authgear/authgear-server/pkg/auth/handler/webapp/viewmodels" "github.com/authgear/authgear-server/pkg/auth/webapp" authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/meter" "github.com/authgear/authgear-server/pkg/util/httputil" "github.com/authgear/authgear-server/pkg/util/template" @@ -115,7 +116,7 @@ func (h *InternalAuthflowV2SignupLoginHandler) ServeHTTP(w http.ResponseWriter, "identification": "oauth", "alias": providerAlias, "redirect_uri": callbackURL, - "response_mode": string(sso.ResponseModeFormPost), + "response_mode": oauthrelyingparty.ResponseModeFormPost, } result, err := h.Controller.ReplaceScreen(r, s, authflow.FlowTypeSignupLogin, input) diff --git a/pkg/auth/handler/webapp/authflowv2/login.go b/pkg/auth/handler/webapp/authflowv2/login.go index 22e12dde50..2a5d4ddb94 100644 --- a/pkg/auth/handler/webapp/authflowv2/login.go +++ b/pkg/auth/handler/webapp/authflowv2/login.go @@ -6,12 +6,13 @@ import ( "net/http" "net/url" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + handlerwebapp "github.com/authgear/authgear-server/pkg/auth/handler/webapp" v2viewmodels "github.com/authgear/authgear-server/pkg/auth/handler/webapp/authflowv2/viewmodels" "github.com/authgear/authgear-server/pkg/auth/handler/webapp/viewmodels" "github.com/authgear/authgear-server/pkg/auth/webapp" authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/meter" "github.com/authgear/authgear-server/pkg/util/httputil" @@ -98,7 +99,7 @@ func (h *AuthflowV2LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Reques "identification": "oauth", "alias": providerAlias, "redirect_uri": callbackURL, - "response_mode": string(sso.ResponseModeFormPost), + "response_mode": oauthrelyingparty.ResponseModeFormPost, } result, err := h.Controller.ReplaceScreen(r, s, authflow.FlowTypeSignupLogin, input) diff --git a/pkg/auth/handler/webapp/authflowv2/promote.go b/pkg/auth/handler/webapp/authflowv2/promote.go index 6291cad848..4def6c608b 100644 --- a/pkg/auth/handler/webapp/authflowv2/promote.go +++ b/pkg/auth/handler/webapp/authflowv2/promote.go @@ -4,11 +4,12 @@ import ( "net/http" "net/url" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + handlerwebapp "github.com/authgear/authgear-server/pkg/auth/handler/webapp" "github.com/authgear/authgear-server/pkg/auth/handler/webapp/viewmodels" "github.com/authgear/authgear-server/pkg/auth/webapp" authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/util/httproute" "github.com/authgear/authgear-server/pkg/util/validation" ) @@ -83,7 +84,7 @@ func (h *AuthflowV2PromoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Requ "identification": "oauth", "alias": providerAlias, "redirect_uri": callbackURL, - "response_mode": string(sso.ResponseModeFormPost), + "response_mode": oauthrelyingparty.ResponseModeFormPost, } result, err := h.Controller.AdvanceWithInput(r, s, screen, input, nil) diff --git a/pkg/auth/handler/webapp/authflowv2/routes.go b/pkg/auth/handler/webapp/authflowv2/routes.go index 83edbd605d..8892fdc9d4 100644 --- a/pkg/auth/handler/webapp/authflowv2/routes.go +++ b/pkg/auth/handler/webapp/authflowv2/routes.go @@ -16,6 +16,7 @@ import ( "github.com/authgear/authgear-server/pkg/lib/authn/otp" "github.com/authgear/authgear-server/pkg/lib/authn/user" "github.com/authgear/authgear-server/pkg/lib/config" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/wechat" ) const ( @@ -282,7 +283,7 @@ func (n *AuthflowV2Navigator) navigateStepIdentify(s *webapp.AuthflowScreenWithF data := s.StateTokenFlowResponse.Action.Data.(declarative.OAuthData) switch data.OAuthProviderType { - case config.OAuthSSOProviderTypeWechat: + case wechat.Type: s.Advance(AuthflowV2RouteWechat, result) default: authorizationURL, _ := url.Parse(data.OAuthAuthorizationURL) diff --git a/pkg/auth/handler/webapp/identity_test.go b/pkg/auth/handler/webapp/identity_test.go index 80fc2604ef..7978879a3b 100644 --- a/pkg/auth/handler/webapp/identity_test.go +++ b/pkg/auth/handler/webapp/identity_test.go @@ -5,10 +5,11 @@ import ( . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/auth/handler/webapp" "github.com/authgear/authgear-server/pkg/lib/authn/identity" - "github.com/authgear/authgear-server/pkg/lib/config" ) func TestIdentitiesDisplayName(t *testing.T) { @@ -22,7 +23,7 @@ func TestIdentitiesDisplayName(t *testing.T) { oauthProviderIdentity := &identity.Info{ Type: model.IdentityTypeOAuth, OAuth: &identity.OAuth{ - ProviderID: config.ProviderID{ + ProviderID: oauthrelyingparty.ProviderID{ Type: "provider", }, Claims: map[string]interface{}{ @@ -34,7 +35,7 @@ func TestIdentitiesDisplayName(t *testing.T) { oauthProviderIdentityWithStandardClaims := &identity.Info{ Type: model.IdentityTypeOAuth, OAuth: &identity.OAuth{ - ProviderID: config.ProviderID{ + ProviderID: oauthrelyingparty.ProviderID{ Type: "provider2", }, Claims: map[string]interface{}{}, diff --git a/pkg/auth/handler/webapp/user_profile_test.go b/pkg/auth/handler/webapp/user_profile_test.go index c00dd3807e..f9f1c676dd 100644 --- a/pkg/auth/handler/webapp/user_profile_test.go +++ b/pkg/auth/handler/webapp/user_profile_test.go @@ -3,9 +3,10 @@ package webapp import ( "testing" + . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" - . "github.com/smartystreets/goconvey/convey" ) func makeFull() *model.User { diff --git a/pkg/auth/handler/webapp/viewmodels/authflow_branch_test.go b/pkg/auth/handler/webapp/viewmodels/authflow_branch_test.go index 93eadec491..67102fb2a8 100644 --- a/pkg/auth/handler/webapp/viewmodels/authflow_branch_test.go +++ b/pkg/auth/handler/webapp/viewmodels/authflow_branch_test.go @@ -3,8 +3,9 @@ package viewmodels import ( "testing" - "github.com/authgear/authgear-server/pkg/lib/config" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/authgear-server/pkg/lib/config" ) func TestAuthflowBranchViewModel(t *testing.T) { diff --git a/pkg/auth/handler/webapp/wire.go b/pkg/auth/handler/webapp/wire.go index 80e8ba1799..fbff236af7 100644 --- a/pkg/auth/handler/webapp/wire.go +++ b/pkg/auth/handler/webapp/wire.go @@ -4,11 +4,12 @@ package webapp import ( + "github.com/google/wire" + "github.com/authgear/authgear-server/pkg/auth/webapp" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/infra/redis/appredis" "github.com/authgear/authgear-server/pkg/util/clock" - "github.com/google/wire" ) func newGlobalSessionService(appID config.AppID, clock clock.Clock, redisHandle *appredis.Handle) *GlobalSessionService { diff --git a/pkg/auth/webapp/authflow_routes.go b/pkg/auth/webapp/authflow_routes.go index efa7be4a50..12910c48c2 100644 --- a/pkg/auth/webapp/authflow_routes.go +++ b/pkg/auth/webapp/authflow_routes.go @@ -14,6 +14,7 @@ import ( "github.com/authgear/authgear-server/pkg/lib/authn/otp" "github.com/authgear/authgear-server/pkg/lib/authn/user" "github.com/authgear/authgear-server/pkg/lib/config" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/wechat" ) const ( @@ -261,7 +262,7 @@ func (n *AuthflowNavigator) navigateStepIdentify(s *AuthflowScreenWithFlowRespon data := s.StateTokenFlowResponse.Action.Data.(declarative.OAuthData) switch data.OAuthProviderType { - case config.OAuthSSOProviderTypeWechat: + case wechat.Type: s.Advance(AuthflowRouteWechat, result) default: authorizationURL, _ := url.Parse(data.OAuthAuthorizationURL) diff --git a/pkg/auth/webapp/dynamic_csp_middleware_test.go b/pkg/auth/webapp/dynamic_csp_middleware_test.go index 00aa716d50..c95bc6409c 100644 --- a/pkg/auth/webapp/dynamic_csp_middleware_test.go +++ b/pkg/auth/webapp/dynamic_csp_middleware_test.go @@ -5,9 +5,10 @@ import ( "net/http/httptest" "testing" - "github.com/authgear/authgear-server/pkg/lib/config" gomock "github.com/golang/mock/gomock" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/authgear-server/pkg/lib/config" ) func TestDynamicCSPMiddleware(t *testing.T) { diff --git a/pkg/auth/webapp/wechat_redirect_uri_middleware.go b/pkg/auth/webapp/wechat_redirect_uri_middleware.go index eed53d79b1..633ed1a64b 100644 --- a/pkg/auth/webapp/wechat_redirect_uri_middleware.go +++ b/pkg/auth/webapp/wechat_redirect_uri_middleware.go @@ -6,6 +6,7 @@ import ( "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/lib/config" + oauthrelyingpartywechat "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/wechat" "github.com/authgear/authgear-server/pkg/util/httputil" "github.com/authgear/authgear-server/pkg/util/wechat" ) @@ -65,7 +66,7 @@ func (m *WeChatRedirectURIMiddleware) Handle(next http.Handler) http.Handler { func (m *WeChatRedirectURIMiddleware) isWechatEnabled() bool { for _, providerConfig := range m.IdentityConfig.OAuth.Providers { - if providerConfig.Type == config.OAuthSSOProviderTypeWechat { + if providerConfig.Type() == oauthrelyingpartywechat.Type { return true } } @@ -82,8 +83,8 @@ func (m *WeChatRedirectURIMiddleware) populateWechatRedirectURI( // Validate x_wechat_redirect_uri valid := false for _, providerConfig := range m.IdentityConfig.OAuth.Providers { - if providerConfig.Type == config.OAuthSSOProviderTypeWechat { - for _, allowed := range providerConfig.WeChatRedirectURIs { + if providerConfig.Type() == oauthrelyingpartywechat.Type { + for _, allowed := range oauthrelyingpartywechat.ProviderConfig(providerConfig).WechatRedirectURIs() { if weChatRedirectURI == allowed { valid = true } diff --git a/pkg/lib/analytic/appdb_store.go b/pkg/lib/analytic/appdb_store.go index 4ce6326cf1..5b015f1d02 100644 --- a/pkg/lib/analytic/appdb_store.go +++ b/pkg/lib/analytic/appdb_store.go @@ -5,6 +5,7 @@ import ( "time" sq "github.com/Masterminds/squirrel" + "github.com/authgear/authgear-server/pkg/lib/infra/db/appdb" ) diff --git a/pkg/lib/analytic/chart_service_test.go b/pkg/lib/analytic/chart_service_test.go index a13e6c4fe4..5a7ca641ec 100644 --- a/pkg/lib/analytic/chart_service_test.go +++ b/pkg/lib/analytic/chart_service_test.go @@ -4,12 +4,13 @@ import ( "testing" "time" + . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/authgear-server/pkg/lib/analytic" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/util/clock" "github.com/authgear/authgear-server/pkg/util/periodical" "github.com/authgear/authgear-server/pkg/util/timeutil" - . "github.com/smartystreets/goconvey/convey" ) func TestChartService(t *testing.T) { diff --git a/pkg/lib/analytic/count.go b/pkg/lib/analytic/count.go index ff8edde158..afce7323c1 100644 --- a/pkg/lib/analytic/count.go +++ b/pkg/lib/analytic/count.go @@ -5,7 +5,7 @@ import ( "time" "github.com/authgear/authgear-server/pkg/api/model" - "github.com/authgear/authgear-server/pkg/lib/config" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty" "github.com/authgear/authgear-server/pkg/util/uuid" ) @@ -40,7 +40,7 @@ func init() { string(typ), fmt.Sprintf(DailySignupWithLoginIDCountType, typ), }) } - for _, typ := range config.OAuthSSOProviderTypes { + for _, typ := range oauthrelyingparty.BuiltinProviderTypes { DailySignupCountTypeByMethods = append(DailySignupCountTypeByMethods, &DailySignupCountTypeByMethod{ string(typ), fmt.Sprintf(DailySignupWithOAuthCountType, typ), }) diff --git a/pkg/lib/analytic/time_test.go b/pkg/lib/analytic/time_test.go index e131544e4f..cc79a5a82c 100644 --- a/pkg/lib/analytic/time_test.go +++ b/pkg/lib/analytic/time_test.go @@ -4,9 +4,10 @@ import ( "testing" "time" + . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/authgear-server/pkg/lib/analytic" "github.com/authgear/authgear-server/pkg/util/periodical" - . "github.com/smartystreets/goconvey/convey" ) func TestGetDateListByRangeInclusive(t *testing.T) { diff --git a/pkg/lib/authenticationflow/allowlist_test.go b/pkg/lib/authenticationflow/allowlist_test.go index 5c10f65540..84a3e3764c 100644 --- a/pkg/lib/authenticationflow/allowlist_test.go +++ b/pkg/lib/authenticationflow/allowlist_test.go @@ -3,8 +3,9 @@ package authenticationflow import ( "testing" - "github.com/authgear/authgear-server/pkg/lib/config" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/authgear-server/pkg/lib/config" ) func TestFlowAllowlist(t *testing.T) { diff --git a/pkg/lib/authenticationflow/declarative/data_account_linking.go b/pkg/lib/authenticationflow/declarative/data_account_linking.go index 2d8092ceaf..43eae89a90 100644 --- a/pkg/lib/authenticationflow/declarative/data_account_linking.go +++ b/pkg/lib/authenticationflow/declarative/data_account_linking.go @@ -12,7 +12,7 @@ type AccountLinkingIdentificationOption struct { Action config.AccountLinkingAction `json:"action"` // ProviderType is specific to OAuth. - ProviderType config.OAuthSSOProviderType `json:"provider_type,omitempty"` + ProviderType string `json:"provider_type,omitempty"` // Alias is specific to OAuth. Alias string `json:"alias,omitempty"` } diff --git a/pkg/lib/authenticationflow/declarative/data_identification.go b/pkg/lib/authenticationflow/declarative/data_identification.go index cc8cca10ea..44ededacc5 100644 --- a/pkg/lib/authenticationflow/declarative/data_identification.go +++ b/pkg/lib/authenticationflow/declarative/data_identification.go @@ -5,6 +5,7 @@ import ( authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" "github.com/authgear/authgear-server/pkg/lib/authn/identity" "github.com/authgear/authgear-server/pkg/lib/config" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/wechat" ) type IdentificationData struct { @@ -25,11 +26,11 @@ type IdentificationOption struct { Identification config.AuthenticationFlowIdentification `json:"identification"` // ProviderType is specific to OAuth. - ProviderType config.OAuthSSOProviderType `json:"provider_type,omitempty"` + ProviderType string `json:"provider_type,omitempty"` // Alias is specific to OAuth. Alias string `json:"alias,omitempty"` // WechatAppType is specific to OAuth. - WechatAppType config.OAuthSSOWeChatAppType `json:"wechat_app_type,omitempty"` + WechatAppType wechat.AppType `json:"wechat_app_type,omitempty"` // WebAuthnRequestOptions is specific to Passkey. RequestOptions *model.WebAuthnRequestOptions `json:"request_options,omitempty"` @@ -50,12 +51,12 @@ func NewIdentificationOptionLoginID(i config.AuthenticationFlowIdentification) I func NewIdentificationOptionsOAuth(oauthConfig *config.OAuthSSOConfig, oauthFeatureConfig *config.OAuthSSOProvidersFeatureConfig) []IdentificationOption { output := []IdentificationOption{} for _, p := range oauthConfig.Providers { - if !identity.IsOAuthSSOProviderTypeDisabled(p.Type, oauthFeatureConfig) { + if !identity.IsOAuthSSOProviderTypeDisabled(p, oauthFeatureConfig) { output = append(output, IdentificationOption{ Identification: config.AuthenticationFlowIdentificationOAuth, - ProviderType: p.Type, - Alias: p.Alias, - WechatAppType: p.AppType, + ProviderType: p.Type(), + Alias: p.Alias(), + WechatAppType: wechat.ProviderConfig(p).AppType(), }) } } diff --git a/pkg/lib/authenticationflow/declarative/data_oauth.go b/pkg/lib/authenticationflow/declarative/data_oauth.go index dece6acbfb..c1d2797112 100644 --- a/pkg/lib/authenticationflow/declarative/data_oauth.go +++ b/pkg/lib/authenticationflow/declarative/data_oauth.go @@ -2,15 +2,15 @@ package declarative import ( authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" - "github.com/authgear/authgear-server/pkg/lib/config" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/wechat" ) type OAuthData struct { TypedData - Alias string `json:"alias,omitempty"` - OAuthProviderType config.OAuthSSOProviderType `json:"oauth_provider_type,omitempty"` - OAuthAuthorizationURL string `json:"oauth_authorization_url,omitempty"` - WechatAppType config.OAuthSSOWeChatAppType `json:"wechat_app_type,omitempty"` + Alias string `json:"alias,omitempty"` + OAuthProviderType string `json:"oauth_provider_type,omitempty"` + OAuthAuthorizationURL string `json:"oauth_authorization_url,omitempty"` + WechatAppType wechat.AppType `json:"wechat_app_type,omitempty"` } var _ authflow.Data = OAuthData{} diff --git a/pkg/lib/authenticationflow/declarative/generate_config_login_flow_test.go b/pkg/lib/authenticationflow/declarative/generate_config_login_flow_test.go index e598f5b6b7..07f7418804 100644 --- a/pkg/lib/authenticationflow/declarative/generate_config_login_flow_test.go +++ b/pkg/lib/authenticationflow/declarative/generate_config_login_flow_test.go @@ -9,6 +9,7 @@ import ( "sigs.k8s.io/yaml" "github.com/authgear/authgear-server/pkg/lib/config" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/google" ) func TestGenerateLoginFlowConfig(t *testing.T) { diff --git a/pkg/lib/authenticationflow/declarative/generate_config_reauth_flow_test.go b/pkg/lib/authenticationflow/declarative/generate_config_reauth_flow_test.go index 261b8ed8cd..da3bcf6f57 100644 --- a/pkg/lib/authenticationflow/declarative/generate_config_reauth_flow_test.go +++ b/pkg/lib/authenticationflow/declarative/generate_config_reauth_flow_test.go @@ -9,6 +9,7 @@ import ( "sigs.k8s.io/yaml" "github.com/authgear/authgear-server/pkg/lib/config" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/google" ) func TestGenerateReauthFlowConfig(t *testing.T) { diff --git a/pkg/lib/authenticationflow/declarative/generate_config_signup_flow_test.go b/pkg/lib/authenticationflow/declarative/generate_config_signup_flow_test.go index d1233eaddf..98187591d4 100644 --- a/pkg/lib/authenticationflow/declarative/generate_config_signup_flow_test.go +++ b/pkg/lib/authenticationflow/declarative/generate_config_signup_flow_test.go @@ -9,6 +9,7 @@ import ( "sigs.k8s.io/yaml" "github.com/authgear/authgear-server/pkg/lib/config" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/google" ) func TestGenerateSignupFlowConfig(t *testing.T) { diff --git a/pkg/lib/authenticationflow/declarative/generate_config_signup_login_flow_test.go b/pkg/lib/authenticationflow/declarative/generate_config_signup_login_flow_test.go index 35c16a64d2..d5fd7aa3e5 100644 --- a/pkg/lib/authenticationflow/declarative/generate_config_signup_login_flow_test.go +++ b/pkg/lib/authenticationflow/declarative/generate_config_signup_login_flow_test.go @@ -9,6 +9,7 @@ import ( "sigs.k8s.io/yaml" "github.com/authgear/authgear-server/pkg/lib/config" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/google" ) func TestGenerateSignupLoginFlowConfig(t *testing.T) { diff --git a/pkg/lib/authenticationflow/declarative/input_account_linking_identification.go b/pkg/lib/authenticationflow/declarative/input_account_linking_identification.go index eb191a4c9a..dfe8b2f470 100644 --- a/pkg/lib/authenticationflow/declarative/input_account_linking_identification.go +++ b/pkg/lib/authenticationflow/declarative/input_account_linking_identification.go @@ -3,10 +3,10 @@ package declarative import ( "encoding/json" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" "github.com/iawaknahc/jsonschema/pkg/jsonpointer" authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/util/validation" ) @@ -43,7 +43,7 @@ func (i *InputSchemaAccountLinkingIdentification) SchemaBuilder() validation.Sch // response_mode is optional. b.Properties().Property("response_mode", validation.SchemaBuilder{}. Type(validation.TypeString). - Enum(sso.ResponseModeFormPost, sso.ResponseModeQuery)) + Enum(oauthrelyingparty.ResponseModeFormPost, oauthrelyingparty.ResponseModeQuery)) } b.Required(required...) oneOf = append(oneOf, b) @@ -70,8 +70,8 @@ func (i *InputSchemaAccountLinkingIdentification) MakeInput(rawMessage json.RawM type InputAccountLinkingIdentification struct { Index int `json:"index,omitempty"` - RedirectURI string `json:"redirect_uri,omitempty"` - ResponseMode sso.ResponseMode `json:"response_mode,omitempty"` + RedirectURI string `json:"redirect_uri,omitempty"` + ResponseMode string `json:"response_mode,omitempty"` } var _ authflow.Input = &InputAccountLinkingIdentification{} @@ -85,6 +85,6 @@ func (i *InputAccountLinkingIdentification) GetAccountLinkingIdentificationIndex func (i *InputAccountLinkingIdentification) GetAccountLinkingOAuthRedirectURI() string { return i.RedirectURI } -func (i *InputAccountLinkingIdentification) GetAccountLinkingOAuthResponseMode() sso.ResponseMode { +func (i *InputAccountLinkingIdentification) GetAccountLinkingOAuthResponseMode() string { return i.ResponseMode } diff --git a/pkg/lib/authenticationflow/declarative/input_interface.go b/pkg/lib/authenticationflow/declarative/input_interface.go index 5a90b7f2b4..71342360d7 100644 --- a/pkg/lib/authenticationflow/declarative/input_interface.go +++ b/pkg/lib/authenticationflow/declarative/input_interface.go @@ -6,7 +6,6 @@ import ( "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/authn/attrs" "github.com/authgear/authgear-server/pkg/lib/authn/identity" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/config" ) @@ -29,7 +28,7 @@ type inputTakeAccountRecoveryDestinationOptionIndex interface { type inputTakeAccountLinkingIdentification interface { GetAccountLinkingIdentificationIndex() int GetAccountLinkingOAuthRedirectURI() string - GetAccountLinkingOAuthResponseMode() sso.ResponseMode + GetAccountLinkingOAuthResponseMode() string } type inputTakeAuthenticationMethod interface { @@ -47,7 +46,7 @@ type inputTakeIDToken interface { type inputTakeOAuthAuthorizationRequest interface { GetOAuthAlias() string GetOAuthRedirectURI() string - GetOAuthResponseMode() sso.ResponseMode + GetOAuthResponseMode() string // We used to accept `state`. // But it turns out to be confusing. // `state` is used to maintain state between the request and the callback. diff --git a/pkg/lib/authenticationflow/declarative/input_login_flow_step_authenticate_test.go b/pkg/lib/authenticationflow/declarative/input_login_flow_step_authenticate_test.go index db1650f435..731a624556 100644 --- a/pkg/lib/authenticationflow/declarative/input_login_flow_step_authenticate_test.go +++ b/pkg/lib/authenticationflow/declarative/input_login_flow_step_authenticate_test.go @@ -4,9 +4,10 @@ import ( "encoding/json" "testing" + . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/util/validation" - . "github.com/smartystreets/goconvey/convey" ) func TestInputSchemaLoginFlowStepAuthenticate(t *testing.T) { diff --git a/pkg/lib/authenticationflow/declarative/input_step_identify.go b/pkg/lib/authenticationflow/declarative/input_step_identify.go index 7ae393bc8b..ce5e42b2eb 100644 --- a/pkg/lib/authenticationflow/declarative/input_step_identify.go +++ b/pkg/lib/authenticationflow/declarative/input_step_identify.go @@ -5,8 +5,9 @@ import ( "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/util/validation" ) @@ -70,7 +71,7 @@ func (i *InputSchemaStepIdentify) SchemaBuilder() validation.SchemaBuilder { // response_mode is optional. b.Properties().Property("response_mode", validation.SchemaBuilder{}. Type(validation.TypeString). - Enum(sso.ResponseModeFormPost, sso.ResponseModeQuery)) + Enum(oauthrelyingparty.ResponseModeFormPost, oauthrelyingparty.ResponseModeQuery)) setRequiredAndAppendOneOf() case config.AuthenticationFlowIdentificationPasskey: @@ -108,9 +109,9 @@ type InputStepIdentify struct { LoginID string `json:"login,omitempty"` - Alias string `json:"alias,omitempty"` - RedirectURI string `json:"redirect_uri,omitempty"` - ResponseMode sso.ResponseMode `json:"response_mode,omitempty"` + Alias string `json:"alias,omitempty"` + RedirectURI string `json:"redirect_uri,omitempty"` + ResponseMode string `json:"response_mode,omitempty"` } var _ authflow.Input = &InputStepIdentify{} @@ -141,6 +142,6 @@ func (i *InputStepIdentify) GetOAuthRedirectURI() string { return i.RedirectURI } -func (i *InputStepIdentify) GetOAuthResponseMode() sso.ResponseMode { +func (i *InputStepIdentify) GetOAuthResponseMode() string { return i.ResponseMode } diff --git a/pkg/lib/authenticationflow/declarative/input_step_identify_test.go b/pkg/lib/authenticationflow/declarative/input_step_identify_test.go index 7d468d3924..45d2e94670 100644 --- a/pkg/lib/authenticationflow/declarative/input_step_identify_test.go +++ b/pkg/lib/authenticationflow/declarative/input_step_identify_test.go @@ -7,6 +7,7 @@ import ( . "github.com/smartystreets/goconvey/convey" "github.com/authgear/authgear-server/pkg/lib/config" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/wechat" ) func TestInputSchemaStepIdentify(t *testing.T) { @@ -36,7 +37,7 @@ func TestInputSchemaStepIdentify(t *testing.T) { { Identification: config.AuthenticationFlowIdentificationOAuth, Alias: "wechat_mobile", - WechatAppType: config.OAuthSSOWeChatAppTypeMobile, + WechatAppType: wechat.AppTypeMobile, }, { Identification: config.AuthenticationFlowIdentificationPasskey, diff --git a/pkg/lib/authenticationflow/declarative/input_take_oauth_authorization_request.go b/pkg/lib/authenticationflow/declarative/input_take_oauth_authorization_request.go index b96cc421c9..2898c3f4f0 100644 --- a/pkg/lib/authenticationflow/declarative/input_take_oauth_authorization_request.go +++ b/pkg/lib/authenticationflow/declarative/input_take_oauth_authorization_request.go @@ -5,8 +5,9 @@ import ( "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/util/validation" ) @@ -34,7 +35,7 @@ func (i *InputSchemaTakeOAuthAuthorizationRequest) SchemaBuilder() validation.Sc b.Properties().Property("redirect_uri", validation.SchemaBuilder{}.Type(validation.TypeString).Format("uri")) b.Properties().Property("response_mode", validation.SchemaBuilder{}. Type(validation.TypeString). - Enum(sso.ResponseModeFormPost, sso.ResponseModeQuery)) + Enum(oauthrelyingparty.ResponseModeFormPost, oauthrelyingparty.ResponseModeQuery)) var enumValues []interface{} for _, c := range i.OAuthOptions { @@ -57,9 +58,9 @@ func (i *InputSchemaTakeOAuthAuthorizationRequest) MakeInput(rawMessage json.Raw } type InputTakeOAuthAuthorizationRequest struct { - Alias string `json:"alias"` - RedirectURI string `json:"redirect_uri"` - ResponseMode sso.ResponseMode `json:"response_mode,omitempty"` + Alias string `json:"alias"` + RedirectURI string `json:"redirect_uri"` + ResponseMode string `json:"response_mode,omitempty"` } var _ authflow.Input = &InputTakeOAuthAuthorizationRequest{} @@ -75,6 +76,6 @@ func (i *InputTakeOAuthAuthorizationRequest) GetOAuthRedirectURI() string { return i.RedirectURI } -func (i *InputTakeOAuthAuthorizationRequest) GetOAuthResponseMode() sso.ResponseMode { +func (i *InputTakeOAuthAuthorizationRequest) GetOAuthResponseMode() string { return i.ResponseMode } diff --git a/pkg/lib/authenticationflow/declarative/input_take_oauth_authorization_request_test.go b/pkg/lib/authenticationflow/declarative/input_take_oauth_authorization_request_test.go index 83f4c022ee..e6954be071 100644 --- a/pkg/lib/authenticationflow/declarative/input_take_oauth_authorization_request_test.go +++ b/pkg/lib/authenticationflow/declarative/input_take_oauth_authorization_request_test.go @@ -7,6 +7,7 @@ import ( . "github.com/smartystreets/goconvey/convey" "github.com/authgear/authgear-server/pkg/lib/config" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/wechat" ) func TestInputSchemaTakeOAuthAuthorizationRequest(t *testing.T) { @@ -27,7 +28,7 @@ func TestInputSchemaTakeOAuthAuthorizationRequest(t *testing.T) { { Identification: config.AuthenticationFlowIdentificationOAuth, Alias: "wechat_mobile", - WechatAppType: config.OAuthSSOWeChatAppTypeMobile, + WechatAppType: wechat.AppTypeMobile, }, }, }, ` diff --git a/pkg/lib/authenticationflow/declarative/input_take_oob_otp_channel_test.go b/pkg/lib/authenticationflow/declarative/input_take_oob_otp_channel_test.go index 60b5c34607..0d488be26e 100644 --- a/pkg/lib/authenticationflow/declarative/input_take_oob_otp_channel_test.go +++ b/pkg/lib/authenticationflow/declarative/input_take_oob_otp_channel_test.go @@ -4,9 +4,10 @@ import ( "encoding/json" "testing" + . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/util/validation" - . "github.com/smartystreets/goconvey/convey" ) func TestInputSchemaTakeOOBOTPChannel(t *testing.T) { diff --git a/pkg/lib/authenticationflow/declarative/intent_account_linking.go b/pkg/lib/authenticationflow/declarative/intent_account_linking.go index f6fec6c8d2..b1c6a007d1 100644 --- a/pkg/lib/authenticationflow/declarative/intent_account_linking.go +++ b/pkg/lib/authenticationflow/declarative/intent_account_linking.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/authgear-server/pkg/api" "github.com/authgear/authgear-server/pkg/api/model" authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" @@ -12,7 +14,6 @@ import ( "github.com/authgear/authgear-server/pkg/lib/infra/mail" "github.com/authgear/authgear-server/pkg/util/phone" "github.com/authgear/authgear-server/pkg/util/slice" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) func init() { @@ -214,7 +215,7 @@ func (i *IntentAccountLinking) getOptions() []AccountLinkingIdentificationOption return slice.FlatMap(i.Conflicts, func(c *AccountLinkingConflict) []AccountLinkingIdentificationOptionInternal { var identifcation config.AuthenticationFlowIdentification var maskedDisplayName string - var providerType config.OAuthSSOProviderType + var providerType string var providerAlias string identity := c.Identity @@ -239,7 +240,7 @@ func (i *IntentAccountLinking) getOptions() []AccountLinkingIdentificationOption } case model.IdentityTypeOAuth: identifcation = config.AuthenticationFlowIdentificationOAuth - providerType = config.OAuthSSOProviderType(identity.OAuth.ProviderID.Type) + providerType = identity.OAuth.ProviderID.Type maskedDisplayName = identity.OAuth.GetDisplayName() providerAlias = identity.OAuth.ProviderAlias default: diff --git a/pkg/lib/authenticationflow/declarative/intent_check_conflict_and_create_identity.go b/pkg/lib/authenticationflow/declarative/intent_check_conflict_and_create_identity.go index 7aed4e8c98..70ac835ec7 100644 --- a/pkg/lib/authenticationflow/declarative/intent_check_conflict_and_create_identity.go +++ b/pkg/lib/authenticationflow/declarative/intent_check_conflict_and_create_identity.go @@ -4,10 +4,11 @@ import ( "context" "fmt" + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/authgear-server/pkg/api/model" authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" "github.com/authgear/authgear-server/pkg/lib/authn/identity" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) func init() { diff --git a/pkg/lib/authenticationflow/declarative/milestone.go b/pkg/lib/authenticationflow/declarative/milestone.go index a4a2fd989f..469e312a5b 100644 --- a/pkg/lib/authenticationflow/declarative/milestone.go +++ b/pkg/lib/authenticationflow/declarative/milestone.go @@ -4,14 +4,14 @@ import ( "context" "sort" + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" "github.com/authgear/authgear-server/pkg/lib/authn/authenticator" "github.com/authgear/authgear-server/pkg/lib/authn/identity" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/session/idpsession" "github.com/authgear/authgear-server/pkg/util/slice" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) func getUserID(flows authflow.Flows) (userID string, err error) { @@ -257,7 +257,7 @@ type MilestoneUseAccountLinkingIdentification interface { MilestoneUseAccountLinkingIdentification() *AccountLinkingConflict MilestoneUseAccountLinkingIdentificationSelectedOption() AccountLinkingIdentificationOption MilestoneUseAccountLinkingIdentificationRedirectURI() string - MilestoneUseAccountLinkingIdentificationResponseMode() sso.ResponseMode + MilestoneUseAccountLinkingIdentificationResponseMode() string } type MilestonePromptCreatePasskey interface { diff --git a/pkg/lib/authenticationflow/declarative/node_do_use_authenticator_password.go b/pkg/lib/authenticationflow/declarative/node_do_use_authenticator_password.go index a215e0dbf6..16df31fd2f 100644 --- a/pkg/lib/authenticationflow/declarative/node_do_use_authenticator_password.go +++ b/pkg/lib/authenticationflow/declarative/node_do_use_authenticator_password.go @@ -1,10 +1,11 @@ package declarative import ( + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" "github.com/authgear/authgear-server/pkg/lib/authn/authenticator" "github.com/authgear/authgear-server/pkg/lib/config" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) func init() { diff --git a/pkg/lib/authenticationflow/declarative/node_lookup_identity_oauth.go b/pkg/lib/authenticationflow/declarative/node_lookup_identity_oauth.go index b62cfe7849..03c20a4e85 100644 --- a/pkg/lib/authenticationflow/declarative/node_lookup_identity_oauth.go +++ b/pkg/lib/authenticationflow/declarative/node_lookup_identity_oauth.go @@ -9,7 +9,6 @@ import ( "github.com/authgear/authgear-server/pkg/api" "github.com/authgear/authgear-server/pkg/api/apierrors" authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/config" ) @@ -22,7 +21,7 @@ type NodeLookupIdentityOAuth struct { SyntheticInput *InputStepIdentify `json:"synthetic_input,omitempty"` Alias string `json:"alias,omitempty"` RedirectURI string `json:"redirect_uri,omitempty"` - ResponseMode sso.ResponseMode `json:"response_mode,omitempty"` + ResponseMode string `json:"response_mode,omitempty"` } var _ authflow.NodeSimple = &NodeLookupIdentityOAuth{} diff --git a/pkg/lib/authenticationflow/declarative/node_oauth.go b/pkg/lib/authenticationflow/declarative/node_oauth.go index dc593288fd..2abb840790 100644 --- a/pkg/lib/authenticationflow/declarative/node_oauth.go +++ b/pkg/lib/authenticationflow/declarative/node_oauth.go @@ -7,7 +7,6 @@ import ( authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" "github.com/authgear/authgear-server/pkg/lib/authn/identity" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" ) func init() { @@ -15,11 +14,11 @@ func init() { } type NodeOAuth struct { - JSONPointer jsonpointer.T `json:"json_pointer,omitempty"` - NewUserID string `json:"new_user_id,omitempty"` - Alias string `json:"alias,omitempty"` - RedirectURI string `json:"redirect_uri,omitempty"` - ResponseMode sso.ResponseMode `json:"response_mode,omitempty"` + JSONPointer jsonpointer.T `json:"json_pointer,omitempty"` + NewUserID string `json:"new_user_id,omitempty"` + Alias string `json:"alias,omitempty"` + RedirectURI string `json:"redirect_uri,omitempty"` + ResponseMode string `json:"response_mode,omitempty"` } var _ authflow.NodeSimple = &NodeOAuth{} diff --git a/pkg/lib/authenticationflow/declarative/node_promote_identity_oauth.go b/pkg/lib/authenticationflow/declarative/node_promote_identity_oauth.go index 216429b1c4..72c8000103 100644 --- a/pkg/lib/authenticationflow/declarative/node_promote_identity_oauth.go +++ b/pkg/lib/authenticationflow/declarative/node_promote_identity_oauth.go @@ -10,7 +10,6 @@ import ( "github.com/authgear/authgear-server/pkg/api/model" authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" "github.com/authgear/authgear-server/pkg/lib/authn/identity" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/util/slice" ) @@ -24,7 +23,7 @@ type NodePromoteIdentityOAuth struct { SyntheticInput *InputStepIdentify `json:"synthetic_input,omitempty"` Alias string `json:"alias,omitempty"` RedirectURI string `json:"redirect_uri,omitempty"` - ResponseMode sso.ResponseMode `json:"response_mode,omitempty"` + ResponseMode string `json:"response_mode,omitempty"` } var _ authflow.NodeSimple = &NodePromoteIdentityOAuth{} diff --git a/pkg/lib/authenticationflow/declarative/node_use_account_linking_identification.go b/pkg/lib/authenticationflow/declarative/node_use_account_linking_identification.go index 97b7f0445e..5ebb360849 100644 --- a/pkg/lib/authenticationflow/declarative/node_use_account_linking_identification.go +++ b/pkg/lib/authenticationflow/declarative/node_use_account_linking_identification.go @@ -2,7 +2,6 @@ package declarative import ( authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" ) func init() { @@ -14,8 +13,8 @@ type NodeUseAccountLinkingIdentification struct { Conflict *AccountLinkingConflict `json:"conflict,omitempty"` // oauth - RedirectURI string `json:"redirect_uri,omitempty"` - ResponseMode sso.ResponseMode `json:"response_mode,omitempty"` + RedirectURI string `json:"redirect_uri,omitempty"` + ResponseMode string `json:"response_mode,omitempty"` } var _ authflow.NodeSimple = &NodeUseAccountLinkingIdentification{} @@ -36,6 +35,6 @@ func (n *NodeUseAccountLinkingIdentification) MilestoneUseAccountLinkingIdentifi func (n *NodeUseAccountLinkingIdentification) MilestoneUseAccountLinkingIdentificationRedirectURI() string { return n.RedirectURI } -func (n *NodeUseAccountLinkingIdentification) MilestoneUseAccountLinkingIdentificationResponseMode() sso.ResponseMode { +func (n *NodeUseAccountLinkingIdentification) MilestoneUseAccountLinkingIdentificationResponseMode() string { return n.ResponseMode } diff --git a/pkg/lib/authenticationflow/declarative/synthetic_input_account_linking_identify.go b/pkg/lib/authenticationflow/declarative/synthetic_input_account_linking_identify.go index 921821cf83..0338167afc 100644 --- a/pkg/lib/authenticationflow/declarative/synthetic_input_account_linking_identify.go +++ b/pkg/lib/authenticationflow/declarative/synthetic_input_account_linking_identify.go @@ -1,7 +1,6 @@ package declarative import ( - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/config" ) @@ -15,7 +14,7 @@ type SyntheticInputAccountLinkingIdentify struct { // For identification=oauth Alias string RedirectURI string - ResponseMode sso.ResponseMode + ResponseMode string } // GetLoginID implements inputTakeLoginID. @@ -39,7 +38,7 @@ func (i *SyntheticInputAccountLinkingIdentify) GetOAuthRedirectURI() string { } // GetOAuthResponseMode implements inputTakeOAuthAuthorizationRequest. -func (i *SyntheticInputAccountLinkingIdentify) GetOAuthResponseMode() sso.ResponseMode { +func (i *SyntheticInputAccountLinkingIdentify) GetOAuthResponseMode() string { return i.ResponseMode } diff --git a/pkg/lib/authenticationflow/declarative/synthetic_input_oauth.go b/pkg/lib/authenticationflow/declarative/synthetic_input_oauth.go index 8cf6f2113f..b812b3eed8 100644 --- a/pkg/lib/authenticationflow/declarative/synthetic_input_oauth.go +++ b/pkg/lib/authenticationflow/declarative/synthetic_input_oauth.go @@ -3,7 +3,6 @@ package declarative import ( authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" "github.com/authgear/authgear-server/pkg/lib/authn/identity" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/config" ) @@ -12,7 +11,7 @@ type SyntheticInputOAuth struct { Alias string `json:"alias,omitempty"` State string `json:"state,omitempty"` RedirectURI string `json:"redirect_uri,omitempty"` - ResponseMode sso.ResponseMode `json:"response_mode,omitempty"` + ResponseMode string `json:"response_mode,omitempty"` IdentitySpec *identity.Spec `json:"identity_spec,omitempty"` } @@ -39,7 +38,7 @@ func (i *SyntheticInputOAuth) GetOAuthRedirectURI() string { return i.RedirectURI } -func (i *SyntheticInputOAuth) GetOAuthResponseMode() sso.ResponseMode { +func (i *SyntheticInputOAuth) GetOAuthResponseMode() string { return i.ResponseMode } diff --git a/pkg/lib/authenticationflow/declarative/synthetic_input_passkey.go b/pkg/lib/authenticationflow/declarative/synthetic_input_passkey.go index a533f652d9..ce9ef8407e 100644 --- a/pkg/lib/authenticationflow/declarative/synthetic_input_passkey.go +++ b/pkg/lib/authenticationflow/declarative/synthetic_input_passkey.go @@ -1,9 +1,10 @@ package declarative import ( + "github.com/go-webauthn/webauthn/protocol" + authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" "github.com/authgear/authgear-server/pkg/lib/config" - "github.com/go-webauthn/webauthn/protocol" ) type SyntheticInputPasskey struct { diff --git a/pkg/lib/authenticationflow/declarative/utils_account_linking.go b/pkg/lib/authenticationflow/declarative/utils_account_linking.go index a3a2ff6d68..83162a2f7d 100644 --- a/pkg/lib/authenticationflow/declarative/utils_account_linking.go +++ b/pkg/lib/authenticationflow/declarative/utils_account_linking.go @@ -3,12 +3,13 @@ package declarative import ( "context" + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/authgear-server/pkg/api" "github.com/authgear/authgear-server/pkg/api/model" authflow "github.com/authgear/authgear-server/pkg/lib/authenticationflow" "github.com/authgear/authgear-server/pkg/lib/authn/identity" "github.com/authgear/authgear-server/pkg/lib/config" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) func resolveAccountLinkingConfigsOAuth( @@ -143,7 +144,7 @@ func linkByOAuthIncomingOAuthSpec( // Not the same type, so must be not identical continue } - if !conflict.Identity.OAuth.ProviderID.Equal(&request.Spec.OAuth.ProviderID) { + if !conflict.Identity.OAuth.ProviderID.Equal(request.Spec.OAuth.ProviderID) { // Not the same provider continue } diff --git a/pkg/lib/authenticationflow/declarative/utils_common.go b/pkg/lib/authenticationflow/declarative/utils_common.go index 6a38330ff4..e76b6237e3 100644 --- a/pkg/lib/authenticationflow/declarative/utils_common.go +++ b/pkg/lib/authenticationflow/declarative/utils_common.go @@ -5,6 +5,9 @@ import ( "errors" "fmt" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/authgear-server/pkg/api" "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/api/model" @@ -12,14 +15,14 @@ import ( "github.com/authgear/authgear-server/pkg/lib/authn/authenticator" "github.com/authgear/authgear-server/pkg/lib/authn/identity" "github.com/authgear/authgear-server/pkg/lib/authn/otp" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/infra/mail" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/wechat" "github.com/authgear/authgear-server/pkg/lib/uiparam" "github.com/authgear/authgear-server/pkg/util/errorutil" "github.com/authgear/authgear-server/pkg/util/phone" "github.com/authgear/authgear-server/pkg/util/uuid" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) func authenticatorIsDefault(deps *authflow.Dependencies, userID string, authenticatorKind model.AuthenticatorKind) (isDefault bool, err error) { @@ -673,25 +676,24 @@ func handleOAuthAuthorizationResponse(deps *authflow.Dependencies, opts HandleOA errorDescription := inputOAuth.GetOAuthErrorDescription() errorURI := inputOAuth.GetOAuthErrorURI() - return nil, sso.NewOAuthError(oauthError, errorDescription, errorURI) - } - - oauthProvider := deps.OAuthProviderFactory.NewOAuthProvider(opts.Alias) - if oauthProvider == nil { - return nil, api.ErrOAuthProviderNotFound + return nil, oauthrelyingpartyutil.NewOAuthError(oauthError, errorDescription, errorURI) } code := inputOAuth.GetOAuthAuthorizationCode() + providerConfig, err := deps.OAuthProviderFactory.GetProviderConfig(opts.Alias) + if err != nil { + return nil, err + } + // TODO(authflow): support nonce but do not save nonce in cookies. // Nonce in the current implementation is stored in cookies. // In the Authentication Flow API, cookies are not sent in Safari in third-party context. emptyNonce := "" - authInfo, err := oauthProvider.GetAuthInfo( - sso.OAuthAuthorizationResponse{ - Code: code, - }, - sso.GetAuthInfoParam{ + authInfo, err := deps.OAuthProviderFactory.GetUserProfile( + opts.Alias, + oauthrelyingparty.GetUserProfileOptions{ + Code: code, RedirectURI: opts.RedirectURI, Nonce: emptyNonce, }, @@ -700,7 +702,6 @@ func handleOAuthAuthorizationResponse(deps *authflow.Dependencies, opts HandleOA return nil, err } - providerConfig := oauthProvider.Config() providerID := providerConfig.ProviderID() identitySpec := &identity.Spec{ Type: model.IdentityTypeOAuth, @@ -708,7 +709,7 @@ func handleOAuthAuthorizationResponse(deps *authflow.Dependencies, opts HandleOA ProviderID: providerID, SubjectID: authInfo.ProviderUserID, RawProfile: authInfo.ProviderRawProfile, - StandardClaims: authInfo.StandardAttributes.ToClaims(), + StandardClaims: authInfo.StandardAttributes, }, } @@ -718,34 +719,33 @@ func handleOAuthAuthorizationResponse(deps *authflow.Dependencies, opts HandleOA type GetOAuthDataOptions struct { RedirectURI string Alias string - ResponseMode sso.ResponseMode + ResponseMode string } func getOAuthData(ctx context.Context, deps *authflow.Dependencies, opts GetOAuthDataOptions) (data OAuthData, err error) { - oauthProvider := deps.OAuthProviderFactory.NewOAuthProvider(opts.Alias) - if oauthProvider == nil { - err = api.ErrOAuthProviderNotFound + providerConfig, err := deps.OAuthProviderFactory.GetProviderConfig(opts.Alias) + if err != nil { return } uiParam := uiparam.GetUIParam(ctx) - param := sso.GetAuthURLParam{ + param := oauthrelyingparty.GetAuthorizationURLOptions{ RedirectURI: opts.RedirectURI, ResponseMode: opts.ResponseMode, Prompt: uiParam.Prompt, } - authorizationURL, err := oauthProvider.GetAuthURL(param) + authorizationURL, err := deps.OAuthProviderFactory.GetAuthorizationURL(opts.Alias, param) if err != nil { return } data = NewOAuthData(OAuthData{ Alias: opts.Alias, - OAuthProviderType: oauthProvider.Config().Type, + OAuthProviderType: providerConfig.Type(), OAuthAuthorizationURL: authorizationURL, - WechatAppType: oauthProvider.Config().AppType, + WechatAppType: wechat.ProviderConfig(providerConfig).AppType(), }) return } diff --git a/pkg/lib/authenticationflow/dependencies.go b/pkg/lib/authenticationflow/dependencies.go index 63281eef30..c9fe325277 100644 --- a/pkg/lib/authenticationflow/dependencies.go +++ b/pkg/lib/authenticationflow/dependencies.go @@ -7,6 +7,8 @@ import ( "github.com/iawaknahc/jsonschema/pkg/jsonpointer" "github.com/lestrrat-go/jwx/v2/jwt" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/api/event" "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/accountmigration" @@ -19,7 +21,6 @@ import ( "github.com/authgear/authgear-server/pkg/lib/authn/identity/anonymous" "github.com/authgear/authgear-server/pkg/lib/authn/mfa" "github.com/authgear/authgear-server/pkg/lib/authn/otp" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/authn/user" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/facade" @@ -190,7 +191,9 @@ type OfflineGrantStore interface { } type OAuthProviderFactory interface { - NewOAuthProvider(alias string) sso.OAuthProvider + GetProviderConfig(alias string) (oauthrelyingparty.ProviderConfig, error) + GetAuthorizationURL(alias string, options oauthrelyingparty.GetAuthorizationURLOptions) (string, error) + GetUserProfile(alias string, options oauthrelyingparty.GetUserProfileOptions) (oauthrelyingparty.UserProfile, error) } type PasskeyRequestOptionsService interface { diff --git a/pkg/lib/authenticationflow/flow.go b/pkg/lib/authenticationflow/flow.go index 7b5e66c5b8..b32463f7fb 100644 --- a/pkg/lib/authenticationflow/flow.go +++ b/pkg/lib/authenticationflow/flow.go @@ -4,8 +4,9 @@ import ( "fmt" "reflect" - "github.com/authgear/authgear-server/pkg/lib/config" "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + + "github.com/authgear/authgear-server/pkg/lib/config" ) // PublicFlow is a instantiable intent by the public. diff --git a/pkg/lib/authenticationflow/service.go b/pkg/lib/authenticationflow/service.go index 0732511c43..730d425216 100644 --- a/pkg/lib/authenticationflow/service.go +++ b/pkg/lib/authenticationflow/service.go @@ -6,10 +6,11 @@ import ( "errors" "net/http" + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/authgear-server/pkg/lib/authn/authenticationinfo" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/util/log" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) //go:generate mockgen -source=service.go -destination=service_mock_test.go -package authenticationflow diff --git a/pkg/lib/authn/authenticator/oob/provider.go b/pkg/lib/authn/authenticator/oob/provider.go index e51ad1371e..c012256be0 100644 --- a/pkg/lib/authn/authenticator/oob/provider.go +++ b/pkg/lib/authn/authenticator/oob/provider.go @@ -4,16 +4,16 @@ import ( "fmt" "sort" + "github.com/authgear/authgear-server/pkg/api/internalinterface" "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/authn/authenticator" - "github.com/authgear/authgear-server/pkg/lib/authn/identity/loginid" "github.com/authgear/authgear-server/pkg/util/clock" "github.com/authgear/authgear-server/pkg/util/uuid" "github.com/authgear/authgear-server/pkg/util/validation" ) type LoginIDNormalizerFactory interface { - NormalizerWithLoginIDType(loginIDKeyType model.LoginIDKeyType) loginid.Normalizer + NormalizerWithLoginIDType(loginIDKeyType model.LoginIDKeyType) internalinterface.LoginIDNormalizer } type Provider struct { diff --git a/pkg/lib/authn/identity/candidate.go b/pkg/lib/authn/identity/candidate.go index b64069c2e6..75a51675fe 100644 --- a/pkg/lib/authn/identity/candidate.go +++ b/pkg/lib/authn/identity/candidate.go @@ -1,10 +1,11 @@ package identity import ( - "fmt" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/config" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/wechat" ) type Candidate map[string]interface{} @@ -27,16 +28,16 @@ const ( CandidateKeyModifyDisabled = "modify_disabled" ) -func NewOAuthCandidate(c *config.OAuthSSOProviderConfig) Candidate { +func NewOAuthCandidate(cfg oauthrelyingparty.ProviderConfig) Candidate { return Candidate{ CandidateKeyIdentityID: "", CandidateKeyType: string(model.IdentityTypeOAuth), - CandidateKeyProviderType: string(c.Type), - CandidateKeyProviderAlias: c.Alias, + CandidateKeyProviderType: string(cfg.Type()), + CandidateKeyProviderAlias: cfg.Alias(), CandidateKeyProviderSubjectID: "", - CandidateKeyProviderAppType: string(c.AppType), + CandidateKeyProviderAppType: string(wechat.ProviderConfig(cfg).AppType()), CandidateKeyDisplayID: "", - CandidateKeyModifyDisabled: *c.ModifyDisabled, + CandidateKeyModifyDisabled: cfg.ModifyDisabled(), } } @@ -60,27 +61,6 @@ func NewSIWECandidate() Candidate { } } -func IsOAuthSSOProviderTypeDisabled(typ config.OAuthSSOProviderType, featureConfig *config.OAuthSSOProvidersFeatureConfig) bool { - switch typ { - case config.OAuthSSOProviderTypeGoogle: - return featureConfig.Google.Disabled - case config.OAuthSSOProviderTypeFacebook: - return featureConfig.Facebook.Disabled - case config.OAuthSSOProviderTypeGithub: - return featureConfig.Github.Disabled - case config.OAuthSSOProviderTypeLinkedIn: - return featureConfig.LinkedIn.Disabled - case config.OAuthSSOProviderTypeAzureADv2: - return featureConfig.Azureadv2.Disabled - case config.OAuthSSOProviderTypeAzureADB2C: - return featureConfig.Azureadb2c.Disabled - case config.OAuthSSOProviderTypeADFS: - return featureConfig.ADFS.Disabled - case config.OAuthSSOProviderTypeApple: - return featureConfig.Apple.Disabled - case config.OAuthSSOProviderTypeWechat: - return featureConfig.Wechat.Disabled - default: - panic(fmt.Sprintf("node: unknown oauth sso type: %T", typ)) - } +func IsOAuthSSOProviderTypeDisabled(cfg oauthrelyingparty.ProviderConfig, featureConfig *config.OAuthSSOProvidersFeatureConfig) bool { + return featureConfig.IsDisabled(cfg) } diff --git a/pkg/lib/authn/identity/info.go b/pkg/lib/authn/identity/info.go index ebfd8c45a5..c8161bd36b 100644 --- a/pkg/lib/authn/identity/info.go +++ b/pkg/lib/authn/identity/info.go @@ -4,6 +4,8 @@ import ( "fmt" "time" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/config" ) @@ -282,17 +284,18 @@ func (i *Info) ModifyDisabled(c *config.IdentityConfig) bool { return *keyConfig.ModifyDisabled case model.IdentityTypeOAuth: alias := i.OAuth.ProviderAlias - var providerConfig *config.OAuthSSOProviderConfig + var providerConfig oauthrelyingparty.ProviderConfig for _, pc := range c.OAuth.Providers { - if pc.Alias == alias { + pcAlias := pc.Alias() + if pcAlias == alias { pcc := pc - providerConfig = &pcc + providerConfig = pcc } } if providerConfig == nil { return true } - return *providerConfig.ModifyDisabled + return providerConfig.ModifyDisabled() case model.IdentityTypeAnonymous: // modify_disabled is only applicable to login_id and oauth. // So we return false here. diff --git a/pkg/lib/authn/identity/info_test.go b/pkg/lib/authn/identity/info_test.go index 4242c5f5f9..b49470e6cb 100644 --- a/pkg/lib/authn/identity/info_test.go +++ b/pkg/lib/authn/identity/info_test.go @@ -5,9 +5,11 @@ import ( "testing" "time" - "github.com/authgear/authgear-server/pkg/api/model" - "github.com/authgear/authgear-server/pkg/lib/config" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + + "github.com/authgear/authgear-server/pkg/api/model" ) func TestInfoJSON(t *testing.T) { @@ -60,7 +62,7 @@ func TestInfoJSON(t *testing.T) { CreatedAt: time.Date(2006, 1, 2, 0, 0, 0, 0, time.UTC), UpdatedAt: time.Date(2006, 1, 2, 0, 0, 0, 0, time.UTC), - ProviderID: config.ProviderID{ + ProviderID: oauthrelyingparty.ProviderID{ Type: "provider", Keys: map[string]interface{}{ "client_id": "client_id", diff --git a/pkg/lib/authn/identity/loginid/normalizer.go b/pkg/lib/authn/identity/loginid/normalizer.go index 34d154afde..868b605876 100644 --- a/pkg/lib/authn/identity/loginid/normalizer.go +++ b/pkg/lib/authn/identity/loginid/normalizer.go @@ -8,21 +8,17 @@ import ( "golang.org/x/text/secure/precis" "golang.org/x/text/unicode/norm" + "github.com/authgear/authgear-server/pkg/api/internalinterface" "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/util/phone" ) -type Normalizer interface { - Normalize(loginID string) (string, error) - ComputeUniqueKey(normalizeLoginID string) (string, error) -} - type NormalizerFactory struct { Config *config.LoginIDConfig } -func (f *NormalizerFactory) NormalizerWithLoginIDType(loginIDKeyType model.LoginIDKeyType) Normalizer { +func (f *NormalizerFactory) NormalizerWithLoginIDType(loginIDKeyType model.LoginIDKeyType) internalinterface.LoginIDNormalizer { switch loginIDKeyType { case model.LoginIDKeyTypeEmail: return &EmailNormalizer{ @@ -43,6 +39,8 @@ type EmailNormalizer struct { Config *config.LoginIDEmailConfig } +var _ internalinterface.LoginIDNormalizer = &EmailNormalizer{} + func (n *EmailNormalizer) Normalize(loginID string) (string, error) { // refs from stdlib // https://golang.org/src/net/mail/message.go?s=5217:5250#L172 @@ -95,6 +93,8 @@ type UsernameNormalizer struct { Config *config.LoginIDUsernameConfig } +var _ internalinterface.LoginIDNormalizer = &UsernameNormalizer{} + func (n *UsernameNormalizer) Normalize(loginID string) (string, error) { loginID = norm.NFKC.String(loginID) @@ -117,6 +117,8 @@ func (n *UsernameNormalizer) ComputeUniqueKey(normalizeLoginID string) (string, type PhoneNumberNormalizer struct { } +var _ internalinterface.LoginIDNormalizer = &PhoneNumberNormalizer{} + func (n *PhoneNumberNormalizer) Normalize(loginID string) (string, error) { e164, err := phone.LegalParser.ParseInputPhoneNumber(loginID) if err != nil { @@ -132,6 +134,8 @@ func (n *PhoneNumberNormalizer) ComputeUniqueKey(normalizeLoginID string) (strin type NullNormalizer struct{} +var _ internalinterface.LoginIDNormalizer = &NullNormalizer{} + func (n *NullNormalizer) Normalize(loginID string) (string, error) { return loginID, nil } diff --git a/pkg/lib/authn/identity/loginid/normalizer_test.go b/pkg/lib/authn/identity/loginid/normalizer_test.go index 8210645fa5..9b56835465 100644 --- a/pkg/lib/authn/identity/loginid/normalizer_test.go +++ b/pkg/lib/authn/identity/loginid/normalizer_test.go @@ -5,6 +5,7 @@ import ( . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/authgear-server/pkg/api/internalinterface" "github.com/authgear/authgear-server/pkg/lib/config" ) @@ -13,7 +14,7 @@ func TestNormalizers(t *testing.T) { LoginID string NormalizedLoginID string } - f := func(c Case, n Normalizer) { + f := func(c Case, n internalinterface.LoginIDNormalizer) { result, _ := n.Normalize(c.LoginID) So(result, ShouldEqual, c.NormalizedLoginID) } diff --git a/pkg/lib/authn/identity/loginid/provider.go b/pkg/lib/authn/identity/loginid/provider.go index 4c74475ec6..34abbae4b7 100644 --- a/pkg/lib/authn/identity/loginid/provider.go +++ b/pkg/lib/authn/identity/loginid/provider.go @@ -4,12 +4,13 @@ import ( "errors" "sort" + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/authn/identity" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/util/clock" "github.com/authgear/authgear-server/pkg/util/uuid" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) type Provider struct { diff --git a/pkg/lib/authn/identity/oauth/provider.go b/pkg/lib/authn/identity/oauth/provider.go index ea53135229..0717d26a4d 100644 --- a/pkg/lib/authn/identity/oauth/provider.go +++ b/pkg/lib/authn/identity/oauth/provider.go @@ -3,11 +3,13 @@ package oauth import ( "sort" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/authgear-server/pkg/lib/authn/identity" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/util/clock" "github.com/authgear/authgear-server/pkg/util/uuid" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) type Provider struct { @@ -50,12 +52,12 @@ func (p *Provider) Get(userID, id string) (*identity.OAuth, error) { return p.Store.Get(userID, id) } -func (p *Provider) GetByProviderSubject(provider config.ProviderID, subjectID string) (*identity.OAuth, error) { - return p.Store.GetByProviderSubject(provider, subjectID) +func (p *Provider) GetByProviderSubject(providerID oauthrelyingparty.ProviderID, subjectID string) (*identity.OAuth, error) { + return p.Store.GetByProviderSubject(providerID, subjectID) } -func (p *Provider) GetByUserProvider(userID string, provider config.ProviderID) (*identity.OAuth, error) { - return p.Store.GetByUserProvider(userID, provider) +func (p *Provider) GetByUserProvider(userID string, providerID oauthrelyingparty.ProviderID) (*identity.OAuth, error) { + return p.Store.GetByUserProvider(userID, providerID) } func (p *Provider) GetMany(ids []string) ([]*identity.OAuth, error) { @@ -64,7 +66,7 @@ func (p *Provider) GetMany(ids []string) ([]*identity.OAuth, error) { func (p *Provider) New( userID string, - provider config.ProviderID, + providerID oauthrelyingparty.ProviderID, subjectID string, profile map[string]interface{}, claims map[string]interface{}, @@ -72,7 +74,7 @@ func (p *Provider) New( i := &identity.OAuth{ ID: uuid.New(), UserID: userID, - ProviderID: provider, + ProviderID: providerID, ProviderSubjectID: subjectID, UserProfile: profile, Claims: claims, @@ -81,8 +83,8 @@ func (p *Provider) New( alias := "" for _, providerConfig := range p.IdentityConfig.OAuth.Providers { providerID := providerConfig.ProviderID() - if providerID.Equal(&i.ProviderID) { - alias = providerConfig.Alias + if providerID.Equal(i.ProviderID) { + alias = providerConfig.Alias() } } if alias != "" { diff --git a/pkg/lib/authn/identity/oauth/store.go b/pkg/lib/authn/identity/oauth/store.go index c63477f6c0..c5bd048b20 100644 --- a/pkg/lib/authn/identity/oauth/store.go +++ b/pkg/lib/authn/identity/oauth/store.go @@ -9,6 +9,8 @@ import ( "github.com/iawaknahc/jsonschema/pkg/jsonpointer" "github.com/lib/pq" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/authn/identity" "github.com/authgear/authgear-server/pkg/lib/config" @@ -75,8 +77,8 @@ func (s *Store) scan(scn db.Scanner) (*identity.OAuth, error) { alias := "" for _, providerConfig := range s.IdentityConfig.OAuth.Providers { providerID := providerConfig.ProviderID() - if providerID.Equal(&i.ProviderID) { - alias = providerConfig.Alias + if providerID.Equal(i.ProviderID) { + alias = providerConfig.Alias() } } if alias != "" { @@ -182,15 +184,15 @@ func (s *Store) Get(userID string, id string) (*identity.OAuth, error) { return s.scan(rows) } -func (s *Store) GetByProviderSubject(provider config.ProviderID, subjectID string) (*identity.OAuth, error) { - providerKeys, err := json.Marshal(provider.Keys) +func (s *Store) GetByProviderSubject(providerID oauthrelyingparty.ProviderID, subjectID string) (*identity.OAuth, error) { + providerKeys, err := json.Marshal(providerID.Keys) if err != nil { return nil, err } q := s.selectQuery().Where( "o.provider_type = ? AND o.provider_keys = ? AND o.provider_user_id = ?", - provider.Type, providerKeys, subjectID) + providerID.Type, providerKeys, subjectID) rows, err := s.SQLExecutor.QueryRowWith(q) if err != nil { return nil, err @@ -199,15 +201,15 @@ func (s *Store) GetByProviderSubject(provider config.ProviderID, subjectID strin return s.scan(rows) } -func (s *Store) GetByUserProvider(userID string, provider config.ProviderID) (*identity.OAuth, error) { - providerKeys, err := json.Marshal(provider.Keys) +func (s *Store) GetByUserProvider(userID string, providerID oauthrelyingparty.ProviderID) (*identity.OAuth, error) { + providerKeys, err := json.Marshal(providerID.Keys) if err != nil { return nil, err } q := s.selectQuery().Where( "o.provider_type = ? AND o.provider_keys = ? AND p.user_id = ?", - provider.Type, providerKeys, userID) + providerID.Type, providerKeys, userID) rows, err := s.SQLExecutor.QueryRowWith(q) if err != nil { return nil, err diff --git a/pkg/lib/authn/identity/oauth_identity.go b/pkg/lib/authn/identity/oauth_identity.go index cd4b699838..e3984c7ceb 100644 --- a/pkg/lib/authn/identity/oauth_identity.go +++ b/pkg/lib/authn/identity/oauth_identity.go @@ -3,21 +3,22 @@ package identity import ( "time" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/api/model" - "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/infra/mail" "github.com/authgear/authgear-server/pkg/util/phone" ) type OAuth struct { - ID string `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - UserID string `json:"user_id"` - ProviderID config.ProviderID `json:"provider_id"` - ProviderSubjectID string `json:"provider_subject_id"` - UserProfile map[string]interface{} `json:"user_profile,omitempty"` - Claims map[string]interface{} `json:"claims,omitempty"` + ID string `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + UserID string `json:"user_id"` + ProviderID oauthrelyingparty.ProviderID `json:"provider_id"` + ProviderSubjectID string `json:"provider_subject_id"` + UserProfile map[string]interface{} `json:"user_profile,omitempty"` + Claims map[string]interface{} `json:"claims,omitempty"` // This is a derived field and NOT persisted to database. // We still include it in JSON serialization so it can be persisted in the graph. ProviderAlias string `json:"provider_alias,omitempty"` diff --git a/pkg/lib/authn/identity/oauth_spec.go b/pkg/lib/authn/identity/oauth_spec.go index 7279234ed6..1e0ff14341 100644 --- a/pkg/lib/authn/identity/oauth_spec.go +++ b/pkg/lib/authn/identity/oauth_spec.go @@ -1,12 +1,12 @@ package identity import ( - "github.com/authgear/authgear-server/pkg/lib/config" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" ) type OAuthSpec struct { - ProviderID config.ProviderID `json:"provider_id"` - SubjectID string `json:"subject_id"` - RawProfile map[string]interface{} `json:"raw_profile,omitempty"` - StandardClaims map[string]interface{} `json:"standard_claims,omitempty"` + ProviderID oauthrelyingparty.ProviderID `json:"provider_id"` + SubjectID string `json:"subject_id"` + RawProfile map[string]interface{} `json:"raw_profile,omitempty"` + StandardClaims map[string]interface{} `json:"standard_claims,omitempty"` } diff --git a/pkg/lib/authn/identity/service/service.go b/pkg/lib/authn/identity/service/service.go index f0da7875c9..6adedbbbb1 100644 --- a/pkg/lib/authn/identity/service/service.go +++ b/pkg/lib/authn/identity/service/service.go @@ -4,11 +4,13 @@ import ( "errors" "fmt" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/authn/identity" "github.com/authgear/authgear-server/pkg/lib/authn/identity/loginid" "github.com/authgear/authgear-server/pkg/lib/config" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) //go:generate mockgen -source=service.go -destination=service_mock_test.go -package service @@ -32,13 +34,13 @@ type OAuthIdentityProvider interface { Get(userID, id string) (*identity.OAuth, error) GetMany(ids []string) ([]*identity.OAuth, error) List(userID string) ([]*identity.OAuth, error) - GetByProviderSubject(provider config.ProviderID, subjectID string) (*identity.OAuth, error) - GetByUserProvider(userID string, provider config.ProviderID) (*identity.OAuth, error) + GetByProviderSubject(providerID oauthrelyingparty.ProviderID, subjectID string) (*identity.OAuth, error) + GetByUserProvider(userID string, providerID oauthrelyingparty.ProviderID) (*identity.OAuth, error) ListByClaim(name string, value string) ([]*identity.OAuth, error) ListByClaimJSONPointer(pointer jsonpointer.T, value string) ([]*identity.OAuth, error) New( userID string, - provider config.ProviderID, + providerID oauthrelyingparty.ProviderID, subjectID string, profile map[string]interface{}, claims map[string]interface{}, @@ -844,14 +846,14 @@ func (s *Service) listOAuthCandidates(oauths []*identity.OAuth) []identity.Candi out := []identity.Candidate{} for _, providerConfig := range s.Identity.OAuth.Providers { pc := providerConfig - if identity.IsOAuthSSOProviderTypeDisabled(pc.Type, s.IdentityFeatureConfig.OAuth.Providers) { + if identity.IsOAuthSSOProviderTypeDisabled(pc, s.IdentityFeatureConfig.OAuth.Providers) { continue } configProviderID := pc.ProviderID() - candidate := identity.NewOAuthCandidate(&pc) + candidate := identity.NewOAuthCandidate(pc) matched := false for _, iden := range oauths { - if iden.ProviderID.Equal(&configProviderID) { + if iden.ProviderID.Equal(configProviderID) { matched = true candidate[identity.CandidateKeyIdentityID] = iden.ID candidate[identity.CandidateKeyProviderSubjectID] = string(iden.ProviderSubjectID) @@ -859,7 +861,7 @@ func (s *Service) listOAuthCandidates(oauths []*identity.OAuth) []identity.Candi } } canAppend := true - if *providerConfig.ModifyDisabled && !matched { + if providerConfig.ModifyDisabled() && !matched { canAppend = false } if canAppend { diff --git a/pkg/lib/authn/identity/service/service_mock_test.go b/pkg/lib/authn/identity/service/service_mock_test.go index 92c21697a1..cc431786e8 100644 --- a/pkg/lib/authn/identity/service/service_mock_test.go +++ b/pkg/lib/authn/identity/service/service_mock_test.go @@ -9,7 +9,7 @@ import ( identity "github.com/authgear/authgear-server/pkg/lib/authn/identity" loginid "github.com/authgear/authgear-server/pkg/lib/authn/identity/loginid" - config "github.com/authgear/authgear-server/pkg/lib/config" + oauthrelyingparty "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" gomock "github.com/golang/mock/gomock" jsonpointer "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) @@ -281,33 +281,33 @@ func (mr *MockOAuthIdentityProviderMockRecorder) Get(userID, id interface{}) *go } // GetByProviderSubject mocks base method. -func (m *MockOAuthIdentityProvider) GetByProviderSubject(provider config.ProviderID, subjectID string) (*identity.OAuth, error) { +func (m *MockOAuthIdentityProvider) GetByProviderSubject(providerID oauthrelyingparty.ProviderID, subjectID string) (*identity.OAuth, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetByProviderSubject", provider, subjectID) + ret := m.ctrl.Call(m, "GetByProviderSubject", providerID, subjectID) ret0, _ := ret[0].(*identity.OAuth) ret1, _ := ret[1].(error) return ret0, ret1 } // GetByProviderSubject indicates an expected call of GetByProviderSubject. -func (mr *MockOAuthIdentityProviderMockRecorder) GetByProviderSubject(provider, subjectID interface{}) *gomock.Call { +func (mr *MockOAuthIdentityProviderMockRecorder) GetByProviderSubject(providerID, subjectID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByProviderSubject", reflect.TypeOf((*MockOAuthIdentityProvider)(nil).GetByProviderSubject), provider, subjectID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByProviderSubject", reflect.TypeOf((*MockOAuthIdentityProvider)(nil).GetByProviderSubject), providerID, subjectID) } // GetByUserProvider mocks base method. -func (m *MockOAuthIdentityProvider) GetByUserProvider(userID string, provider config.ProviderID) (*identity.OAuth, error) { +func (m *MockOAuthIdentityProvider) GetByUserProvider(userID string, providerID oauthrelyingparty.ProviderID) (*identity.OAuth, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetByUserProvider", userID, provider) + ret := m.ctrl.Call(m, "GetByUserProvider", userID, providerID) ret0, _ := ret[0].(*identity.OAuth) ret1, _ := ret[1].(error) return ret0, ret1 } // GetByUserProvider indicates an expected call of GetByUserProvider. -func (mr *MockOAuthIdentityProviderMockRecorder) GetByUserProvider(userID, provider interface{}) *gomock.Call { +func (mr *MockOAuthIdentityProviderMockRecorder) GetByUserProvider(userID, providerID interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByUserProvider", reflect.TypeOf((*MockOAuthIdentityProvider)(nil).GetByUserProvider), userID, provider) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByUserProvider", reflect.TypeOf((*MockOAuthIdentityProvider)(nil).GetByUserProvider), userID, providerID) } // GetMany mocks base method. @@ -371,17 +371,17 @@ func (mr *MockOAuthIdentityProviderMockRecorder) ListByClaimJSONPointer(pointer, } // New mocks base method. -func (m *MockOAuthIdentityProvider) New(userID string, provider config.ProviderID, subjectID string, profile, claims map[string]interface{}) *identity.OAuth { +func (m *MockOAuthIdentityProvider) New(userID string, providerID oauthrelyingparty.ProviderID, subjectID string, profile, claims map[string]interface{}) *identity.OAuth { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "New", userID, provider, subjectID, profile, claims) + ret := m.ctrl.Call(m, "New", userID, providerID, subjectID, profile, claims) ret0, _ := ret[0].(*identity.OAuth) return ret0 } // New indicates an expected call of New. -func (mr *MockOAuthIdentityProviderMockRecorder) New(userID, provider, subjectID, profile, claims interface{}) *gomock.Call { +func (mr *MockOAuthIdentityProviderMockRecorder) New(userID, providerID, subjectID, profile, claims interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockOAuthIdentityProvider)(nil).New), userID, provider, subjectID, profile, claims) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockOAuthIdentityProvider)(nil).New), userID, providerID, subjectID, profile, claims) } // Update mocks base method. diff --git a/pkg/lib/authn/identity/service/service_test.go b/pkg/lib/authn/identity/service/service_test.go index a23f1fe332..5e198491e6 100644 --- a/pkg/lib/authn/identity/service/service_test.go +++ b/pkg/lib/authn/identity/service/service_test.go @@ -7,9 +7,12 @@ import ( "github.com/golang/mock/gomock" . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/authn/identity" "github.com/authgear/authgear-server/pkg/lib/config" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/google" ) func newBool(b bool) *bool { @@ -53,11 +56,11 @@ func TestProviderListCandidates(t *testing.T) { Convey("oauth", func() { p.Authentication.Identities = []model.IdentityType{model.IdentityTypeOAuth} - p.Identity.OAuth.Providers = []config.OAuthSSOProviderConfig{ + p.Identity.OAuth.Providers = []oauthrelyingparty.ProviderConfig{ { - Alias: "google", - Type: "google", - ModifyDisabled: newBool(false), + "alias": "google", + "type": google.Type, + "modify_disabled": false, }, } @@ -103,11 +106,11 @@ func TestProviderListCandidates(t *testing.T) { }) Convey("respect authentication", func() { - p.Identity.OAuth.Providers = []config.OAuthSSOProviderConfig{ + p.Identity.OAuth.Providers = []oauthrelyingparty.ProviderConfig{ { - Alias: "google", - Type: "google", - ModifyDisabled: newBool(false), + "alias": "google", + "type": google.Type, + "modify_disabled": false, }, } p.Identity.LoginID.Keys = []config.LoginIDKeyConfig{ @@ -167,11 +170,11 @@ func TestProviderListCandidates(t *testing.T) { userID := "a" p.Authentication.Identities = []model.IdentityType{model.IdentityTypeOAuth} - p.Identity.OAuth.Providers = []config.OAuthSSOProviderConfig{ + p.Identity.OAuth.Providers = []oauthrelyingparty.ProviderConfig{ { - Alias: "google", - Type: "google", - ModifyDisabled: newBool(false), + "alias": "google", + "type": google.Type, + "modify_disabled": false, }, } @@ -179,8 +182,8 @@ func TestProviderListCandidates(t *testing.T) { siweProvider.EXPECT().List(userID).Return(nil, nil) oauthProvider.EXPECT().List(userID).Return([]*identity.OAuth{ { - ProviderID: config.ProviderID{ - Type: "google", + ProviderID: oauthrelyingparty.ProviderID{ + Type: google.Type, Keys: map[string]interface{}{}, }, ProviderSubjectID: "john.doe@gmail.com", diff --git a/pkg/lib/authn/otp/code.go b/pkg/lib/authn/otp/code.go index 6fc2fef162..f12bdca0b3 100644 --- a/pkg/lib/authn/otp/code.go +++ b/pkg/lib/authn/otp/code.go @@ -3,8 +3,9 @@ package otp import ( "time" - "github.com/authgear/authgear-server/pkg/util/duration" "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + + "github.com/authgear/authgear-server/pkg/util/duration" ) const ( diff --git a/pkg/lib/authn/otp/service.go b/pkg/lib/authn/otp/service.go index e2092b1986..768ac6e4cb 100644 --- a/pkg/lib/authn/otp/service.go +++ b/pkg/lib/authn/otp/service.go @@ -4,12 +4,13 @@ import ( "errors" "time" + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/ratelimit" "github.com/authgear/authgear-server/pkg/util/clock" "github.com/authgear/authgear-server/pkg/util/httputil" "github.com/authgear/authgear-server/pkg/util/log" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) type GenerateOptions struct { diff --git a/pkg/lib/authn/sso/adfs.go b/pkg/lib/authn/sso/adfs.go deleted file mode 100644 index 977afa512f..0000000000 --- a/pkg/lib/authn/sso/adfs.go +++ /dev/null @@ -1,151 +0,0 @@ -package sso - -import ( - "context" - - "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" - "github.com/authgear/authgear-server/pkg/lib/config" - "github.com/authgear/authgear-server/pkg/util/clock" - "github.com/authgear/authgear-server/pkg/util/validation" -) - -type ADFSImpl struct { - Clock clock.Clock - ProviderConfig config.OAuthSSOProviderConfig - Credentials config.OAuthSSOProviderCredentialsItem - StandardAttributesNormalizer StandardAttributesNormalizer - HTTPClient OAuthHTTPClient -} - -func (*ADFSImpl) Type() config.OAuthSSOProviderType { - return config.OAuthSSOProviderTypeADFS -} - -func (f *ADFSImpl) Config() config.OAuthSSOProviderConfig { - return f.ProviderConfig -} - -func (f *ADFSImpl) getOpenIDConfiguration() (*OIDCDiscoveryDocument, error) { - endpoint := f.ProviderConfig.DiscoveryDocumentEndpoint - return FetchOIDCDiscoveryDocument(f.HTTPClient, endpoint) -} - -func (f *ADFSImpl) GetAuthURL(param GetAuthURLParam) (string, error) { - c, err := f.getOpenIDConfiguration() - if err != nil { - return "", err - } - return c.MakeOAuthURL(AuthorizationURLParams{ - ClientID: f.ProviderConfig.ClientID, - RedirectURI: param.RedirectURI, - Scope: f.ProviderConfig.Type.Scope(), - ResponseType: ResponseTypeCode, - ResponseMode: param.ResponseMode, - State: param.State, - Prompt: f.GetPrompt(param.Prompt), - Nonce: param.Nonce, - }), nil -} - -func (f *ADFSImpl) GetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - return f.OpenIDConnectGetAuthInfo(r, param) -} - -func (f *ADFSImpl) OpenIDConnectGetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - c, err := f.getOpenIDConfiguration() - if err != nil { - return - } - - // OPTIMIZE(sso): Cache JWKs - keySet, err := c.FetchJWKs(f.HTTPClient) - if err != nil { - return - } - - var tokenResp AccessTokenResp - jwtToken, err := c.ExchangeCode( - f.HTTPClient, - f.Clock, - r.Code, - keySet, - f.ProviderConfig.ClientID, - f.Credentials.ClientSecret, - param.RedirectURI, - param.Nonce, - &tokenResp, - ) - if err != nil { - return - } - - claims, err := jwtToken.AsMap(context.TODO()) - if err != nil { - return - } - - sub, ok := claims["sub"].(string) - if !ok { - err = OAuthProtocolError.New("sub not found in ID token") - return - } - - // The upn claim is documented here. - // https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/configuring-alternate-login-id - upn, ok := claims["upn"].(string) - if !ok { - err = OAuthProtocolError.New("upn not found in ID token") - return - } - - extracted, err := stdattrs.Extract(claims, stdattrs.ExtractOptions{}) - if err != nil { - return - } - - // Transform upn into preferred_username - if _, ok := extracted[stdattrs.PreferredUsername]; !ok { - extracted[stdattrs.PreferredUsername] = upn - } - // Transform upn into email - if _, ok := extracted[stdattrs.Email]; !ok { - if emailErr := (validation.FormatEmail{}).CheckFormat(upn); emailErr == nil { - // upn looks like an email address. - extracted[stdattrs.Email] = upn - } - } - - extracted, err = stdattrs.Extract(extracted, stdattrs.ExtractOptions{ - EmailRequired: *f.ProviderConfig.Claims.Email.Required, - }) - if err != nil { - return - } - authInfo.StandardAttributes = extracted - - authInfo.ProviderRawProfile = claims - authInfo.ProviderUserID = sub - - err = f.StandardAttributesNormalizer.Normalize(authInfo.StandardAttributes) - if err != nil { - return - } - - return -} - -func (f *ADFSImpl) GetPrompt(prompt []string) []string { - // adfs only support prompt=login - // ref: https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/ad-fs-prompt-login - for _, p := range prompt { - if p == "login" { - return []string{"login"} - } - } - return []string{} -} - -var ( - _ OAuthProvider = &ADFSImpl{} - _ OpenIDConnectProvider = &ADFSImpl{} -) diff --git a/pkg/lib/authn/sso/apple.go b/pkg/lib/authn/sso/apple.go deleted file mode 100644 index 34bf6d8d4e..0000000000 --- a/pkg/lib/authn/sso/apple.go +++ /dev/null @@ -1,177 +0,0 @@ -package sso - -import ( - "context" - "strings" - - "github.com/lestrrat-go/jwx/v2/jwa" - "github.com/lestrrat-go/jwx/v2/jwk" - "github.com/lestrrat-go/jwx/v2/jwt" - - "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" - "github.com/authgear/authgear-server/pkg/lib/config" - "github.com/authgear/authgear-server/pkg/util/clock" - "github.com/authgear/authgear-server/pkg/util/crypto" - "github.com/authgear/authgear-server/pkg/util/duration" - "github.com/authgear/authgear-server/pkg/util/jwtutil" -) - -var appleOIDCConfig = OIDCDiscoveryDocument{ - JWKSUri: "https://appleid.apple.com/auth/keys", - TokenEndpoint: "https://appleid.apple.com/auth/token", - AuthorizationEndpoint: "https://appleid.apple.com/auth/authorize", -} - -type AppleImpl struct { - Clock clock.Clock - ProviderConfig config.OAuthSSOProviderConfig - Credentials config.OAuthSSOProviderCredentialsItem - StandardAttributesNormalizer StandardAttributesNormalizer - HTTPClient OAuthHTTPClient -} - -func (f *AppleImpl) createClientSecret() (clientSecret string, err error) { - // https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens - key, err := crypto.ParseAppleP8PrivateKey([]byte(f.Credentials.ClientSecret)) - if err != nil { - return - } - - now := f.Clock.NowUTC() - - payload := jwt.New() - _ = payload.Set(jwt.IssuerKey, f.ProviderConfig.TeamID) - _ = payload.Set(jwt.IssuedAtKey, now.Unix()) - _ = payload.Set(jwt.ExpirationKey, now.Add(duration.Short).Unix()) - _ = payload.Set(jwt.AudienceKey, "https://appleid.apple.com") - _ = payload.Set(jwt.SubjectKey, f.ProviderConfig.ClientID) - - jwkKey, err := jwk.FromRaw(key) - if err != nil { - return - } - _ = jwkKey.Set("kid", f.ProviderConfig.KeyID) - - token, err := jwtutil.Sign(payload, jwa.ES256, jwkKey) - if err != nil { - return - } - - clientSecret = string(token) - return -} - -func (*AppleImpl) Type() config.OAuthSSOProviderType { - return config.OAuthSSOProviderTypeApple -} - -func (f *AppleImpl) Config() config.OAuthSSOProviderConfig { - return f.ProviderConfig -} - -func (f *AppleImpl) GetAuthURL(param GetAuthURLParam) (string, error) { - return appleOIDCConfig.MakeOAuthURL(AuthorizationURLParams{ - ClientID: f.ProviderConfig.ClientID, - RedirectURI: param.RedirectURI, - Scope: f.ProviderConfig.Type.Scope(), - ResponseType: ResponseTypeCode, - ResponseMode: param.ResponseMode, - State: param.State, - Prompt: f.GetPrompt(param.Prompt), - Nonce: param.Nonce, - }), nil -} - -func (f *AppleImpl) GetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - return f.OpenIDConnectGetAuthInfo(r, param) -} - -func (f *AppleImpl) OpenIDConnectGetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - keySet, err := appleOIDCConfig.FetchJWKs(f.HTTPClient) - if err != nil { - return - } - - clientSecret, err := f.createClientSecret() - if err != nil { - return - } - - var tokenResp AccessTokenResp - jwtToken, err := appleOIDCConfig.ExchangeCode( - f.HTTPClient, - f.Clock, - r.Code, - keySet, - f.ProviderConfig.ClientID, - clientSecret, - param.RedirectURI, - param.Nonce, - &tokenResp, - ) - if err != nil { - return - } - - claims, err := jwtToken.AsMap(context.TODO()) - if err != nil { - return - } - - // Verify the issuer - // https://developer.apple.com/documentation/signinwithapplerestapi/verifying_a_user - // The exact spec is - // Verify that the iss field contains https://appleid.apple.com - // Therefore, we use strings.Contains here. - iss, ok := claims["iss"].(string) - if !ok { - err = OAuthProtocolError.New("iss not found in ID token") - return - } - if !strings.Contains(iss, "https://appleid.apple.com") { - err = OAuthProtocolError.New("iss does not equal to `https://appleid.apple.com`") - return - } - - // Ensure sub exists - sub, ok := claims["sub"].(string) - if !ok { - err = OAuthProtocolError.New("sub not found in ID Token") - return - } - - // By observation, if the first time of authentication does NOT include the `name` scope, - // Even the Services ID is unauthorized on https://appleid.apple.com, - // and the `name` scope is included, - // The ID Token still does not include the `name` claim. - - authInfo.ProviderRawProfile = claims - authInfo.ProviderUserID = sub - - stdAttrs, err := stdattrs.Extract(claims, stdattrs.ExtractOptions{ - EmailRequired: *f.ProviderConfig.Claims.Email.Required, - }) - if err != nil { - return - } - authInfo.StandardAttributes = stdAttrs.WithNameCopiedToGivenName() - - err = f.StandardAttributesNormalizer.Normalize(authInfo.StandardAttributes) - if err != nil { - return - } - - return -} - -func (f *AppleImpl) GetPrompt(prompt []string) []string { - // apple doesn't support prompt parameter - // see "Send the Required Query Parameters" section for supporting parameters - // ref: https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms - return []string{} -} - -var ( - _ OAuthProvider = &AppleImpl{} - _ OpenIDConnectProvider = &AppleImpl{} -) diff --git a/pkg/lib/authn/sso/authinfo.go b/pkg/lib/authn/sso/authinfo.go deleted file mode 100644 index 42a8a53a50..0000000000 --- a/pkg/lib/authn/sso/authinfo.go +++ /dev/null @@ -1,13 +0,0 @@ -package sso - -import ( - "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" -) - -type AuthInfo struct { - ProviderRawProfile map[string]interface{} - // ProviderUserID is not necessarily equal to sub. - // If there exists a more unique identifier than sub, that identifier is chosen instead. - ProviderUserID string - StandardAttributes stdattrs.T -} diff --git a/pkg/lib/authn/sso/azureadb2c.go b/pkg/lib/authn/sso/azureadb2c.go deleted file mode 100644 index 684cfd91fd..0000000000 --- a/pkg/lib/authn/sso/azureadb2c.go +++ /dev/null @@ -1,201 +0,0 @@ -package sso - -import ( - "context" - "fmt" - - "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" - "github.com/authgear/authgear-server/pkg/lib/config" - "github.com/authgear/authgear-server/pkg/util/clock" -) - -type Azureadb2cImpl struct { - Clock clock.Clock - ProviderConfig config.OAuthSSOProviderConfig - Credentials config.OAuthSSOProviderCredentialsItem - StandardAttributesNormalizer StandardAttributesNormalizer - HTTPClient OAuthHTTPClient -} - -func (f *Azureadb2cImpl) getOpenIDConfiguration() (*OIDCDiscoveryDocument, error) { - tenant := f.ProviderConfig.Tenant - policy := f.ProviderConfig.Policy - - endpoint := fmt.Sprintf( - "https://%s.b2clogin.com/%s.onmicrosoft.com/%s/v2.0/.well-known/openid-configuration", - tenant, - tenant, - policy, - ) - - return FetchOIDCDiscoveryDocument(f.HTTPClient, endpoint) -} - -func (f *Azureadb2cImpl) Type() config.OAuthSSOProviderType { - return config.OAuthSSOProviderTypeAzureADB2C -} - -func (f *Azureadb2cImpl) Config() config.OAuthSSOProviderConfig { - return f.ProviderConfig -} - -func (f *Azureadb2cImpl) GetAuthURL(param GetAuthURLParam) (string, error) { - c, err := f.getOpenIDConfiguration() - if err != nil { - return "", err - } - return c.MakeOAuthURL(AuthorizationURLParams{ - ClientID: f.ProviderConfig.ClientID, - RedirectURI: param.RedirectURI, - Scope: f.ProviderConfig.Type.Scope(), - ResponseType: ResponseTypeCode, - ResponseMode: param.ResponseMode, - State: param.State, - Prompt: f.GetPrompt(param.Prompt), - Nonce: param.Nonce, - }), nil -} - -func (f *Azureadb2cImpl) GetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - return f.OpenIDConnectGetAuthInfo(r, param) -} - -func (f *Azureadb2cImpl) OpenIDConnectGetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - c, err := f.getOpenIDConfiguration() - if err != nil { - return - } - // OPTIMIZE(sso): Cache JWKs - keySet, err := c.FetchJWKs(f.HTTPClient) - if err != nil { - return - } - - var tokenResp AccessTokenResp - jwtToken, err := c.ExchangeCode( - f.HTTPClient, - f.Clock, - r.Code, - keySet, - f.ProviderConfig.ClientID, - f.Credentials.ClientSecret, - param.RedirectURI, - param.Nonce, - &tokenResp, - ) - if err != nil { - return - } - - claims, err := jwtToken.AsMap(context.TODO()) - if err != nil { - return - } - - iss, ok := claims["iss"].(string) - if !ok { - err = OAuthProtocolError.New("iss not found in ID token") - return - } - if iss != c.Issuer { - err = OAuthProtocolError.New( - fmt.Sprintf("iss: %v != %v", iss, c.Issuer), - ) - return - } - - sub, ok := claims["sub"].(string) - if !ok || sub == "" { - err = OAuthProtocolError.New("sub not found in ID Token") - return - } - - authInfo.ProviderRawProfile = claims - authInfo.ProviderUserID = sub - - stdAttrs, err := f.Extract(claims) - if err != nil { - return - } - authInfo.StandardAttributes = stdAttrs - - err = f.StandardAttributesNormalizer.Normalize(authInfo.StandardAttributes) - if err != nil { - return - } - - return -} - -func (f *Azureadb2cImpl) Extract(claims map[string]interface{}) (stdattrs.T, error) { - // Here is the list of possible builtin claims of user flows - // https://learn.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview#user-flows - // city: free text - // country: free text - // jobTitle: free text - // legalAgeGroupClassification: a enum with undocumented variants - // postalCode: free text - // state: free text - // streetAddress: free text - // newUser: true means the user signed up newly - // oid: sub is identical to it by default. - // emails: if non-empty, the first value corresponds to standard claim - // name: correspond to standard claim - // given_name: correspond to standard claim - // family_name: correspond to standard claim - - // For custom policy we further recognize the following claims. - // https://learn.microsoft.com/en-us/azure/active-directory-b2c/user-profile-attributes - // signInNames.emailAddress: string - - extractString := func(input map[string]interface{}, output stdattrs.T, key string) { - if value, ok := input[key].(string); ok && value != "" { - output[key] = value - } - } - - out := stdattrs.T{} - - extractString(claims, out, stdattrs.Name) - extractString(claims, out, stdattrs.GivenName) - extractString(claims, out, stdattrs.FamilyName) - - var email string - if email == "" { - if ifaceSlice, ok := claims["emails"].([]interface{}); ok { - for _, iface := range ifaceSlice { - if str, ok := iface.(string); ok && str != "" { - email = str - } - } - } - } - if email == "" { - if str, ok := claims["signInNames.emailAddress"].(string); ok { - if str != "" { - email = str - } - } - } - out[stdattrs.Email] = email - - return stdattrs.Extract(out, stdattrs.ExtractOptions{ - EmailRequired: *f.ProviderConfig.Claims.Email.Required, - }) -} - -func (f *Azureadb2cImpl) GetPrompt(prompt []string) []string { - // The only supported value is login. - // See https://docs.microsoft.com/en-us/azure/active-directory-b2c/openid-connect - for _, p := range prompt { - if p == "login" { - return []string{"login"} - } - } - return []string{} -} - -var ( - _ OAuthProvider = &Azureadb2cImpl{} - _ OpenIDConnectProvider = &Azureadb2cImpl{} -) diff --git a/pkg/lib/authn/sso/azureadv2.go b/pkg/lib/authn/sso/azureadv2.go deleted file mode 100644 index ea9d522a7d..0000000000 --- a/pkg/lib/authn/sso/azureadv2.go +++ /dev/null @@ -1,176 +0,0 @@ -package sso - -import ( - "context" - "fmt" - - "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" - "github.com/authgear/authgear-server/pkg/lib/config" - "github.com/authgear/authgear-server/pkg/util/clock" -) - -type Azureadv2Impl struct { - Clock clock.Clock - ProviderConfig config.OAuthSSOProviderConfig - Credentials config.OAuthSSOProviderCredentialsItem - StandardAttributesNormalizer StandardAttributesNormalizer - HTTPClient OAuthHTTPClient -} - -func (f *Azureadv2Impl) getOpenIDConfiguration() (*OIDCDiscoveryDocument, error) { - // OPTIMIZE(sso): Cache OpenID configuration - - tenant := f.ProviderConfig.Tenant - var endpoint string - // Azure special tenant - // - // If the azure tenant is `organizations` or `common`, - // the developer should make use of `before_user_create` and `before_identity_create` hook - // to disallow any undesire identity. - // The `raw_profile` of the identity is the ID Token claims. - // Refer to https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens - // to see what claims the token could contain. - // - // For `organizations`, the user can be any user of any organizational AD. - // Therefore the developer should have a whitelist of AD tenant IDs. - // In the incoming hook, check if `tid` matches one of the entry of the whitelist. - // - // For `common`, in addition to the users from `organizations`, any Microsoft personal account - // could be the user. - // In case of personal account, the `tid` is "9188040d-6c67-4c5b-b112-36a304b66dad". - // Therefore the developer should first check if `tid` indicates personal account. - // If yes, apply their logic to disallow the user creation. - // One very common example is to look at the claim `email`. - // Use a email address parser to parse the email address. - // Obtain the domain and check if the domain is whitelisted. - // For example, if the developer only wants user from hotmail.com to create user, - // ensure `tid` is "9188040d-6c67-4c5b-b112-36a304b66dad" and ensure `email` - // is of domain `@hotmail.com`. - - // As of 2019-09-23, two special values are observed. - // To discover these values, create a new client - // and try different options. - switch tenant { - // Special value for any organizational AD - case "organizations": - endpoint = "https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration" - // Special value for any organizational AD and personal accounts (Xbox etc) - case "common": - endpoint = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration" - default: - endpoint = fmt.Sprintf("https://login.microsoftonline.com/%s/v2.0/.well-known/openid-configuration", tenant) - } - - return FetchOIDCDiscoveryDocument(f.HTTPClient, endpoint) -} - -func (*Azureadv2Impl) Type() config.OAuthSSOProviderType { - return config.OAuthSSOProviderTypeAzureADv2 -} - -func (f *Azureadv2Impl) Config() config.OAuthSSOProviderConfig { - return f.ProviderConfig -} - -func (f *Azureadv2Impl) GetAuthURL(param GetAuthURLParam) (string, error) { - c, err := f.getOpenIDConfiguration() - if err != nil { - return "", err - } - return c.MakeOAuthURL(AuthorizationURLParams{ - ClientID: f.ProviderConfig.ClientID, - RedirectURI: param.RedirectURI, - Scope: f.ProviderConfig.Type.Scope(), - ResponseType: ResponseTypeCode, - ResponseMode: param.ResponseMode, - State: param.State, - Prompt: f.GetPrompt(param.Prompt), - Nonce: param.Nonce, - }), nil -} - -func (f *Azureadv2Impl) GetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - return f.OpenIDConnectGetAuthInfo(r, param) -} - -func (f *Azureadv2Impl) OpenIDConnectGetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - c, err := f.getOpenIDConfiguration() - if err != nil { - return - } - // OPTIMIZE(sso): Cache JWKs - keySet, err := c.FetchJWKs(f.HTTPClient) - if err != nil { - return - } - - var tokenResp AccessTokenResp - jwtToken, err := c.ExchangeCode( - f.HTTPClient, - f.Clock, - r.Code, - keySet, - f.ProviderConfig.ClientID, - f.Credentials.ClientSecret, - param.RedirectURI, - param.Nonce, - &tokenResp, - ) - if err != nil { - return - } - - claims, err := jwtToken.AsMap(context.TODO()) - if err != nil { - return - } - - oid, ok := claims["oid"].(string) - if !ok { - err = OAuthProtocolError.New("oid not found in ID Token") - return - } - // For "Microsoft Account", email usually exists. - // For "AD guest user", email usually exists because to invite an user, the inviter must provide email. - // For "AD user", email never exists even one is provided in "Authentication Methods". - - authInfo.ProviderRawProfile = claims - authInfo.ProviderUserID = oid - stdAttrs, err := stdattrs.Extract(claims, stdattrs.ExtractOptions{ - EmailRequired: *f.ProviderConfig.Claims.Email.Required, - }) - if err != nil { - return - } - authInfo.StandardAttributes = stdAttrs - - err = f.StandardAttributesNormalizer.Normalize(authInfo.StandardAttributes) - if err != nil { - return - } - - return -} - -func (f *Azureadv2Impl) GetPrompt(prompt []string) []string { - // Azureadv2 only support single value for prompt - // the first supporting value in the list will be used - // the usage of `none` is for checking existing authentication and/or consent - // which doesn't fit auth ui case - // ref: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow - for _, p := range prompt { - if p == "login" { - return []string{"login"} - } else if p == "consent" { - return []string{"consent"} - } else if p == "select_account" { - return []string{"select_account"} - } - } - return []string{} -} - -var ( - _ OAuthProvider = &Azureadv2Impl{} - _ OpenIDConnectProvider = &Azureadv2Impl{} -) diff --git a/pkg/lib/authn/sso/deps.go b/pkg/lib/authn/sso/deps.go index e29bacb6a5..d14d125b3a 100644 --- a/pkg/lib/authn/sso/deps.go +++ b/pkg/lib/authn/sso/deps.go @@ -7,8 +7,9 @@ import ( "net/url" "os" - "github.com/authgear/authgear-server/pkg/lib/config" "github.com/google/wire" + + "github.com/authgear/authgear-server/pkg/lib/config" ) func ProvideOAuthHTTPClient(env *config.EnvironmentConfig) OAuthHTTPClient { diff --git a/pkg/lib/authn/sso/error.go b/pkg/lib/authn/sso/error.go index e5d5f4114d..e8cfb7af8d 100644 --- a/pkg/lib/authn/sso/error.go +++ b/pkg/lib/authn/sso/error.go @@ -6,29 +6,3 @@ import ( var InvalidConfiguration = apierrors.InternalError.WithReason("InvalidConfiguration") var OAuthProtocolError = apierrors.BadRequest.WithReason("OAuthProtocolError") - -var OAuthError = apierrors.BadRequest.WithReason("OAuthError") - -func NewOAuthError(errorString string, errorDescription string, errorURI string) error { - msg := errorString - if errorDescription != "" { - msg += ": " + errorDescription - } - - return OAuthError.NewWithInfo(msg, apierrors.Details{ - "error": errorString, - "error_description": errorDescription, - "error_uri": errorURI, - }) -} - -// oauthErrorResp is a helper struct for deserialization purpose. -type oauthErrorResp struct { - Error string `json:"error"` - ErrorDescription string `json:"error_description,omitempty"` - ErrorURI string `json:"error_uri,omitempty"` -} - -func (r *oauthErrorResp) AsError() error { - return NewOAuthError(r.Error, r.ErrorDescription, r.ErrorURI) -} diff --git a/pkg/lib/authn/sso/facebook.go b/pkg/lib/authn/sso/facebook.go deleted file mode 100644 index 2dfc9da550..0000000000 --- a/pkg/lib/authn/sso/facebook.go +++ /dev/null @@ -1,147 +0,0 @@ -package sso - -import ( - "net/url" - - "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" - "github.com/authgear/authgear-server/pkg/lib/config" - "github.com/authgear/authgear-server/pkg/util/crypto" -) - -const ( - facebookAuthorizationURL string = "https://www.facebook.com/v11.0/dialog/oauth" - // nolint: gosec - facebookTokenURL string = "https://graph.facebook.com/v11.0/oauth/access_token" - facebookUserInfoURL string = "https://graph.facebook.com/v11.0/me?fields=id,email,first_name,last_name,middle_name,name,name_format,picture,short_name" -) - -type FacebookImpl struct { - ProviderConfig config.OAuthSSOProviderConfig - Credentials config.OAuthSSOProviderCredentialsItem - StandardAttributesNormalizer StandardAttributesNormalizer - HTTPClient OAuthHTTPClient -} - -func (*FacebookImpl) Type() config.OAuthSSOProviderType { - return config.OAuthSSOProviderTypeFacebook -} - -func (f *FacebookImpl) Config() config.OAuthSSOProviderConfig { - return f.ProviderConfig -} - -func (f *FacebookImpl) GetAuthURL(param GetAuthURLParam) (string, error) { - return MakeAuthorizationURL(facebookAuthorizationURL, AuthorizationURLParams{ - ClientID: f.ProviderConfig.ClientID, - RedirectURI: param.RedirectURI, - Scope: f.ProviderConfig.Type.Scope(), - ResponseType: ResponseTypeCode, - // ResponseMode is unset - State: param.State, - Prompt: f.GetPrompt(param.Prompt), - // Nonce is unset - }.Query()), nil -} - -func (f *FacebookImpl) GetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - return f.NonOpenIDConnectGetAuthInfo(r, param) -} - -func (f *FacebookImpl) NonOpenIDConnectGetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - authInfo = AuthInfo{} - - accessTokenResp, err := fetchAccessTokenResp( - f.HTTPClient, - r.Code, - facebookTokenURL, - param.RedirectURI, - f.ProviderConfig.ClientID, - f.Credentials.ClientSecret, - ) - if err != nil { - return - } - - userProfileURL, err := url.Parse(facebookUserInfoURL) - if err != nil { - return - } - q := userProfileURL.Query() - appSecretProof := crypto.HMACSHA256String([]byte(f.Credentials.ClientSecret), []byte(accessTokenResp.AccessToken())) - q.Set("appsecret_proof", appSecretProof) - userProfileURL.RawQuery = q.Encode() - - // Here is the refacted user profile of Louis' facebook account. - // { - // "id": "redacted", - // "email": "redacted", - // "first_name": "Jonathan", - // "last_name": "Doe", - // "name": "Johnathan Doe", - // "name_format": "{first} {last}", - // "picture": { - // "data": { - // "height": 50, - // "is_silhouette": true, - // "url": "http://example.com", - // "width": 50 - // } - // }, - // "short_name": "John" - // } - - userProfile, err := fetchUserProfile(f.HTTPClient, accessTokenResp, userProfileURL.String()) - if err != nil { - return - } - authInfo.ProviderRawProfile = userProfile - - id, _ := userProfile["id"].(string) - email, _ := userProfile["email"].(string) - firstName, _ := userProfile["first_name"].(string) - lastName, _ := userProfile["last_name"].(string) - name, _ := userProfile["name"].(string) - shortName, _ := userProfile["short_name"].(string) - var picture string - if pictureObj, ok := userProfile["picture"].(map[string]interface{}); ok { - if data, ok := pictureObj["data"].(map[string]interface{}); ok { - if url, ok := data["url"].(string); ok { - picture = url - } - } - } - - authInfo.ProviderUserID = id - stdAttrs, err := stdattrs.Extract(map[string]interface{}{ - stdattrs.Email: email, - stdattrs.GivenName: firstName, - stdattrs.FamilyName: lastName, - stdattrs.Name: name, - stdattrs.Nickname: shortName, - stdattrs.Picture: picture, - }, stdattrs.ExtractOptions{ - EmailRequired: *f.ProviderConfig.Claims.Email.Required, - }) - if err != nil { - return - } - authInfo.StandardAttributes = stdAttrs - - err = f.StandardAttributesNormalizer.Normalize(authInfo.StandardAttributes) - if err != nil { - return - } - - return -} - -func (f *FacebookImpl) GetPrompt(prompt []string) []string { - // facebook doesn't support prompt parameter - // https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/ - return []string{} -} - -var ( - _ OAuthProvider = &FacebookImpl{} - _ NonOpenIDConnectProvider = &FacebookImpl{} -) diff --git a/pkg/lib/authn/sso/github.go b/pkg/lib/authn/sso/github.go deleted file mode 100644 index a212cf62c4..0000000000 --- a/pkg/lib/authn/sso/github.go +++ /dev/null @@ -1,181 +0,0 @@ -package sso - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - - "github.com/authgear/authgear-server/pkg/api/apierrors" - "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" - "github.com/authgear/authgear-server/pkg/lib/config" - "github.com/authgear/authgear-server/pkg/util/errorutil" -) - -const ( - githubAuthorizationURL string = "https://github.com/login/oauth/authorize" - // nolint: gosec - githubTokenURL string = "https://github.com/login/oauth/access_token" - githubUserInfoURL string = "https://api.github.com/user" -) - -type GithubImpl struct { - ProviderConfig config.OAuthSSOProviderConfig - Credentials config.OAuthSSOProviderCredentialsItem - StandardAttributesNormalizer StandardAttributesNormalizer - HTTPClient OAuthHTTPClient -} - -func (*GithubImpl) Type() config.OAuthSSOProviderType { - return config.OAuthSSOProviderTypeGithub -} - -func (g *GithubImpl) Config() config.OAuthSSOProviderConfig { - return g.ProviderConfig -} - -func (g *GithubImpl) GetAuthURL(param GetAuthURLParam) (string, error) { - // https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#1-request-a-users-github-identity - return MakeAuthorizationURL(githubAuthorizationURL, AuthorizationURLParams{ - ClientID: g.ProviderConfig.ClientID, - RedirectURI: param.RedirectURI, - Scope: g.ProviderConfig.Type.Scope(), - // ResponseType is unset. - // ResponseMode is unset. - State: param.State, - // Prompt is unset. - // Nonce is unset. - }.Query()), nil -} - -func (g *GithubImpl) GetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - return g.NonOpenIDConnectGetAuthInfo(r, param) -} - -func (g *GithubImpl) NonOpenIDConnectGetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - accessTokenResp, err := g.exchangeCode(r, param) - if err != nil { - return - } - - userProfile, err := g.fetchUserInfo(accessTokenResp) - if err != nil { - return - } - authInfo.ProviderRawProfile = userProfile - - idJSONNumber, _ := userProfile["id"].(json.Number) - email, _ := userProfile["email"].(string) - login, _ := userProfile["login"].(string) - picture, _ := userProfile["avatar_url"].(string) - profile, _ := userProfile["html_url"].(string) - - id := string(idJSONNumber) - - authInfo.ProviderUserID = id - stdAttrs, err := stdattrs.Extract(map[string]interface{}{ - stdattrs.Email: email, - stdattrs.Name: login, - stdattrs.GivenName: login, - stdattrs.Picture: picture, - stdattrs.Profile: profile, - }, stdattrs.ExtractOptions{ - EmailRequired: *g.ProviderConfig.Claims.Email.Required, - }) - if err != nil { - err = apierrors.AddDetails(err, errorutil.Details{ - "ProviderType": apierrors.APIErrorDetail.Value(g.ProviderConfig.Type), - }) - return - } - authInfo.StandardAttributes = stdAttrs - - err = g.StandardAttributesNormalizer.Normalize(authInfo.StandardAttributes) - if err != nil { - return - } - - return -} - -func (g *GithubImpl) exchangeCode(r OAuthAuthorizationResponse, param GetAuthInfoParam) (accessTokenResp AccessTokenResp, err error) { - q := make(url.Values) - q.Set("client_id", g.ProviderConfig.ClientID) - q.Set("client_secret", g.Credentials.ClientSecret) - q.Set("code", r.Code) - q.Set("redirect_uri", param.RedirectURI) - - body := strings.NewReader(q.Encode()) - req, _ := http.NewRequest("POST", githubTokenURL, body) - // https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#2-users-are-redirected-back-to-your-site-by-github - req.Header.Set("Accept", "application/json") - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - resp, err := g.HTTPClient.Do(req) - if err != nil { - return - } - defer resp.Body.Close() - - if resp.StatusCode == 200 { - err = json.NewDecoder(resp.Body).Decode(&accessTokenResp) - if err != nil { - return - } - } else { - var errResp oauthErrorResp - err = json.NewDecoder(resp.Body).Decode(&errResp) - if err != nil { - return - } - err = errResp.AsError() - } - - return -} - -func (g *GithubImpl) fetchUserInfo(accessTokenResp AccessTokenResp) (userProfile map[string]interface{}, err error) { - tokenType := accessTokenResp.TokenType() - accessTokenValue := accessTokenResp.AccessToken() - authorizationHeader := fmt.Sprintf("%s %s", tokenType, accessTokenValue) - - req, err := http.NewRequest(http.MethodGet, githubUserInfoURL, nil) - if err != nil { - return - } - req.Header.Add("Authorization", authorizationHeader) - - resp, err := g.HTTPClient.Do(req) - if resp != nil { - defer resp.Body.Close() - } - if err != nil { - return - } - - if resp.StatusCode != 200 { - err = fmt.Errorf("failed to fetch user profile: unexpected status code: %d", resp.StatusCode) - return - } - - decoder := json.NewDecoder(resp.Body) - // Deserialize "id" as json.Number. - decoder.UseNumber() - err = decoder.Decode(&userProfile) - if err != nil { - return - } - - return -} - -func (*GithubImpl) GetPrompt(prompt []string) []string { - // Github does not support prompt. - return []string{} -} - -var ( - _ OAuthProvider = &GithubImpl{} - _ NonOpenIDConnectProvider = &GithubImpl{} -) diff --git a/pkg/lib/authn/sso/google.go b/pkg/lib/authn/sso/google.go deleted file mode 100644 index beaa7939e6..0000000000 --- a/pkg/lib/authn/sso/google.go +++ /dev/null @@ -1,146 +0,0 @@ -package sso - -import ( - "context" - - "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" - "github.com/authgear/authgear-server/pkg/lib/config" - "github.com/authgear/authgear-server/pkg/util/clock" -) - -const ( - googleOIDCDiscoveryDocumentURL string = "https://accounts.google.com/.well-known/openid-configuration" -) - -type GoogleImpl struct { - Clock clock.Clock - ProviderConfig config.OAuthSSOProviderConfig - Credentials config.OAuthSSOProviderCredentialsItem - StandardAttributesNormalizer StandardAttributesNormalizer - HTTPClient OAuthHTTPClient -} - -func (f *GoogleImpl) GetAuthURL(param GetAuthURLParam) (string, error) { - d, err := FetchOIDCDiscoveryDocument(f.HTTPClient, googleOIDCDiscoveryDocumentURL) - if err != nil { - return "", err - } - return d.MakeOAuthURL(AuthorizationURLParams{ - ClientID: f.ProviderConfig.ClientID, - RedirectURI: param.RedirectURI, - Scope: f.ProviderConfig.Type.Scope(), - ResponseType: ResponseTypeCode, - ResponseMode: param.ResponseMode, - State: param.State, - Nonce: param.Nonce, - Prompt: f.GetPrompt(param.Prompt), - }), nil -} - -func (*GoogleImpl) Type() config.OAuthSSOProviderType { - return config.OAuthSSOProviderTypeGoogle -} - -func (f *GoogleImpl) Config() config.OAuthSSOProviderConfig { - return f.ProviderConfig -} - -func (f *GoogleImpl) GetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - return f.OpenIDConnectGetAuthInfo(r, param) -} - -func (f *GoogleImpl) OpenIDConnectGetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - d, err := FetchOIDCDiscoveryDocument(f.HTTPClient, googleOIDCDiscoveryDocumentURL) - if err != nil { - return - } - // OPTIMIZE(sso): Cache JWKs - keySet, err := d.FetchJWKs(f.HTTPClient) - if err != nil { - return - } - - var tokenResp AccessTokenResp - jwtToken, err := d.ExchangeCode( - f.HTTPClient, - f.Clock, - r.Code, - keySet, - f.ProviderConfig.ClientID, - f.Credentials.ClientSecret, - param.RedirectURI, - param.Nonce, - &tokenResp, - ) - if err != nil { - return - } - - claims, err := jwtToken.AsMap(context.TODO()) - if err != nil { - return - } - - // Verify the issuer - // https://developers.google.com/identity/protocols/OpenIDConnect#validatinganidtoken - iss, ok := claims["iss"].(string) - if !ok { - err = OAuthProtocolError.New("iss not found in ID token") - return - } - if iss != "https://accounts.google.com" && iss != "accounts.google.com" { - err = OAuthProtocolError.New("iss is not from Google") - return - } - - // Ensure sub exists - sub, ok := claims["sub"].(string) - if !ok { - err = OAuthProtocolError.New("sub not found in ID token") - return - } - - authInfo.ProviderRawProfile = claims - authInfo.ProviderUserID = sub - // Google supports - // given_name, family_name, email, picture, profile, locale - // https://developers.google.com/identity/protocols/oauth2/openid-connect#obtainuserinfo - stdAttrs, err := stdattrs.Extract(claims, stdattrs.ExtractOptions{ - EmailRequired: *f.ProviderConfig.Claims.Email.Required, - }) - if err != nil { - return - } - authInfo.StandardAttributes = stdAttrs - - err = f.StandardAttributesNormalizer.Normalize(authInfo.StandardAttributes) - if err != nil { - return - } - - return -} - -func (f *GoogleImpl) GetPrompt(prompt []string) []string { - // google support `none`, `concent` and `select_account` for prompt - // the usage of `none` is for checking existing authentication and/or consent - // which doesn't fit auth ui case - // ref: https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters - newPrompt := []string{} - for _, p := range prompt { - if p == "consent" || - p == "select_account" { - newPrompt = append(newPrompt, p) - } - } - if len(newPrompt) == 0 { - // default - return []string{"select_account"} - } - return newPrompt -} - -var ( - _ OAuthProvider = &GoogleImpl{} - _ OpenIDConnectProvider = &GoogleImpl{} -) diff --git a/pkg/lib/authn/sso/oauth_provider.go b/pkg/lib/authn/sso/oauth_provider.go index 3430f62e36..834baf1a06 100644 --- a/pkg/lib/authn/sso/oauth_provider.go +++ b/pkg/lib/authn/sso/oauth_provider.go @@ -1,58 +1,14 @@ package sso import ( + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + + "github.com/authgear/authgear-server/pkg/api" "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/util/clock" ) -type GetAuthURLParam struct { - RedirectURI string - ResponseMode ResponseMode - Nonce string - State string - Prompt []string -} - -type GetAuthInfoParam struct { - RedirectURI string - Nonce string -} - -type OAuthAuthorizationResponse struct { - Code string -} - -// OAuthProvider is OAuth 2.0 based provider. -type OAuthProvider interface { - Type() config.OAuthSSOProviderType - Config() config.OAuthSSOProviderConfig - GetAuthURL(param GetAuthURLParam) (url string, err error) - GetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (AuthInfo, error) - GetPrompt(prompt []string) []string -} - -// NonOpenIDConnectProvider are OAuth 2.0 provider that does not -// implement OpenID Connect or we do not implement yet. -// They are -// "facebook" -// "linkedin" -// "wechat" -type NonOpenIDConnectProvider interface { - NonOpenIDConnectGetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) -} - -// OpenIDConnectProvider are OpenID Connect provider. -// They are -// "google" -// "apple" -// "azureadv2" -// "azureadb2c" -// "adfs" -type OpenIDConnectProvider interface { - OpenIDConnectGetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) -} - type StandardAttributesNormalizer interface { Normalize(stdattrs.T) error } @@ -65,85 +21,61 @@ type OAuthProviderFactory struct { HTTPClient OAuthHTTPClient } -func (p *OAuthProviderFactory) NewOAuthProvider(alias string) OAuthProvider { +func (p *OAuthProviderFactory) GetProviderConfig(alias string) (oauthrelyingparty.ProviderConfig, error) { providerConfig, ok := p.IdentityConfig.OAuth.GetProviderConfig(alias) if !ok { - return nil + return nil, api.ErrOAuthProviderNotFound + } + return providerConfig, nil +} + +func (p *OAuthProviderFactory) getProvider(alias string) (provider oauthrelyingparty.Provider, deps *oauthrelyingparty.Dependencies, err error) { + providerConfig, err := p.GetProviderConfig(alias) + if err != nil { + return } + credentials, ok := p.Credentials.Lookup(alias) if !ok { - return nil + err = api.ErrOAuthProviderNotFound + return + } + + deps = &oauthrelyingparty.Dependencies{ + Clock: p.Clock, + ProviderConfig: providerConfig, + ClientSecret: credentials.ClientSecret, + HTTPClient: p.HTTPClient.Client, } - switch providerConfig.Type { - case config.OAuthSSOProviderTypeGoogle: - return &GoogleImpl{ - Clock: p.Clock, - ProviderConfig: *providerConfig, - Credentials: *credentials, - StandardAttributesNormalizer: p.StandardAttributesNormalizer, - HTTPClient: p.HTTPClient, - } - case config.OAuthSSOProviderTypeFacebook: - return &FacebookImpl{ - ProviderConfig: *providerConfig, - Credentials: *credentials, - StandardAttributesNormalizer: p.StandardAttributesNormalizer, - HTTPClient: p.HTTPClient, - } - case config.OAuthSSOProviderTypeGithub: - return &GithubImpl{ - ProviderConfig: *providerConfig, - Credentials: *credentials, - StandardAttributesNormalizer: p.StandardAttributesNormalizer, - HTTPClient: p.HTTPClient, - } - case config.OAuthSSOProviderTypeLinkedIn: - return &LinkedInImpl{ - ProviderConfig: *providerConfig, - Credentials: *credentials, - StandardAttributesNormalizer: p.StandardAttributesNormalizer, - HTTPClient: p.HTTPClient, - } - case config.OAuthSSOProviderTypeAzureADv2: - return &Azureadv2Impl{ - Clock: p.Clock, - ProviderConfig: *providerConfig, - Credentials: *credentials, - StandardAttributesNormalizer: p.StandardAttributesNormalizer, - HTTPClient: p.HTTPClient, - } - case config.OAuthSSOProviderTypeAzureADB2C: - return &Azureadb2cImpl{ - Clock: p.Clock, - ProviderConfig: *providerConfig, - Credentials: *credentials, - StandardAttributesNormalizer: p.StandardAttributesNormalizer, - HTTPClient: p.HTTPClient, - } - case config.OAuthSSOProviderTypeADFS: - return &ADFSImpl{ - Clock: p.Clock, - ProviderConfig: *providerConfig, - Credentials: *credentials, - StandardAttributesNormalizer: p.StandardAttributesNormalizer, - HTTPClient: p.HTTPClient, - } - case config.OAuthSSOProviderTypeApple: - return &AppleImpl{ - Clock: p.Clock, - ProviderConfig: *providerConfig, - Credentials: *credentials, - StandardAttributesNormalizer: p.StandardAttributesNormalizer, - HTTPClient: p.HTTPClient, - } - case config.OAuthSSOProviderTypeWechat: - return &WechatImpl{ - ProviderConfig: *providerConfig, - Credentials: *credentials, - StandardAttributesNormalizer: p.StandardAttributesNormalizer, - HTTPClient: p.HTTPClient, - } + provider = providerConfig.MustGetProvider() + return +} + +func (p *OAuthProviderFactory) GetAuthorizationURL(alias string, options oauthrelyingparty.GetAuthorizationURLOptions) (url string, err error) { + provider, deps, err := p.getProvider(alias) + if err != nil { + return } - return nil + + return provider.GetAuthorizationURL(*deps, options) +} + +func (p *OAuthProviderFactory) GetUserProfile(alias string, options oauthrelyingparty.GetUserProfileOptions) (userProfile oauthrelyingparty.UserProfile, err error) { + provider, deps, err := p.getProvider(alias) + if err != nil { + return + } + + userProfile, err = provider.GetUserProfile(*deps, options) + if err != nil { + return + } + + err = p.StandardAttributesNormalizer.Normalize(userProfile.StandardAttributes) + if err != nil { + return + } + + return } diff --git a/pkg/lib/authn/sso/wechat.go b/pkg/lib/authn/sso/wechat.go deleted file mode 100644 index bb0ef2567c..0000000000 --- a/pkg/lib/authn/sso/wechat.go +++ /dev/null @@ -1,131 +0,0 @@ -package sso - -import ( - "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" - "github.com/authgear/authgear-server/pkg/lib/config" -) - -const ( - wechatAuthorizationURL string = "https://open.weixin.qq.com/connect/oauth2/authorize" -) - -type WechatImpl struct { - ProviderConfig config.OAuthSSOProviderConfig - Credentials config.OAuthSSOProviderCredentialsItem - StandardAttributesNormalizer StandardAttributesNormalizer - HTTPClient OAuthHTTPClient -} - -func (*WechatImpl) Type() config.OAuthSSOProviderType { - return config.OAuthSSOProviderTypeWechat -} - -func (w *WechatImpl) Config() config.OAuthSSOProviderConfig { - return w.ProviderConfig -} - -func (w *WechatImpl) GetAuthURL(param GetAuthURLParam) (string, error) { - return MakeAuthorizationURL(wechatAuthorizationURL, AuthorizationURLParams{ - // ClientID is not used by wechat. - WechatAppID: w.ProviderConfig.ClientID, - RedirectURI: param.RedirectURI, - Scope: w.ProviderConfig.Type.Scope(), - ResponseType: ResponseTypeCode, - // ResponseMode is unset. - State: param.State, - Prompt: w.GetPrompt(param.Prompt), - // Nonce is unset. - }.Query()), nil -} - -func (w *WechatImpl) GetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (AuthInfo, error) { - return w.NonOpenIDConnectGetAuthInfo(r, param) -} - -func (w *WechatImpl) NonOpenIDConnectGetAuthInfo(r OAuthAuthorizationResponse, _ GetAuthInfoParam) (authInfo AuthInfo, err error) { - accessTokenResp, err := wechatFetchAccessTokenResp( - w.HTTPClient, - r.Code, - w.ProviderConfig.ClientID, - w.Credentials.ClientSecret, - ) - if err != nil { - return - } - - rawProfile, err := wechatFetchUserProfile(w.HTTPClient, accessTokenResp) - if err != nil { - return - } - - config := w.Config() - var userID string - if config.IsSandboxAccount { - if accessTokenResp.UnionID() != "" { - err = InvalidConfiguration.New("invalid is_sandbox_account config, WeChat sandbox account should not have union id") - return - } - userID = accessTokenResp.OpenID() - } else { - userID = accessTokenResp.UnionID() - } - - if userID == "" { - // this may happen if developer misconfigure is_sandbox_account, e.g. sandbox account doesn't have union id - err = InvalidConfiguration.New("invalid is_sandbox_account config, missing user id in wechat token response") - return - } - - // https://developers.weixin.qq.com/doc/offiaccount/User_Management/Get_users_basic_information_UnionID.html - // Here is an example of how the raw profile looks like. - // { - // "sex": 0, - // "city": "", - // "openid": "redacted", - // "country": "", - // "language": "zh_CN", - // "nickname": "John Doe", - // "province": "", - // "privilege": [], - // "headimgurl": "" - // } - var gender string - if sex, ok := rawProfile["sex"].(float64); ok { - if sex == 1 { - gender = "male" - } else if sex == 2 { - gender = "female" - } - } - - name, _ := rawProfile["nickname"].(string) - locale, _ := rawProfile["language"].(string) - - authInfo.ProviderRawProfile = rawProfile - authInfo.ProviderUserID = userID - - // Claims.Email.Required is not respected because wechat does not return the email claim. - authInfo.StandardAttributes = stdattrs.T{ - stdattrs.Name: name, - stdattrs.Locale: locale, - stdattrs.Gender: gender, - }.WithNameCopiedToGivenName() - - err = w.StandardAttributesNormalizer.Normalize(authInfo.StandardAttributes) - if err != nil { - return - } - - return -} - -func (w *WechatImpl) GetPrompt(prompt []string) []string { - // wechat doesn't support prompt parameter - // ref: https://developers.weixin.qq.com/doc/oplatform/en/Third-party_Platforms/Official_Accounts/official_account_website_authorization.html - return []string{} -} - -var ( - _ OAuthProvider = &WechatImpl{} - _ NonOpenIDConnectProvider = &WechatImpl{} -) diff --git a/pkg/lib/authn/sso/wechat_authinfo.go b/pkg/lib/authn/sso/wechat_authinfo.go deleted file mode 100644 index 4dd94c5e75..0000000000 --- a/pkg/lib/authn/sso/wechat_authinfo.go +++ /dev/null @@ -1,158 +0,0 @@ -package sso - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "net/url" -) - -const ( - // nolint: gosec - wechatAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token" - wechatUserInfoURL = "https://api.weixin.qq.com/sns/userinfo" -) - -type wechatOAuthErrorResp struct { - ErrorCode int `json:"errcode"` - ErrorMsg string `json:"errmsg"` -} - -func (r *wechatOAuthErrorResp) AsError() error { - return fmt.Errorf("wechat: %d: %s", r.ErrorCode, r.ErrorMsg) -} - -type wechatAccessTokenResp map[string]interface{} - -func (r wechatAccessTokenResp) AccessToken() string { - accessToken, ok := r["access_token"].(string) - if ok { - return accessToken - } - return "" -} - -func (r wechatAccessTokenResp) OpenID() string { - openid, ok := r["openid"].(string) - if ok { - return openid - } - return "" -} - -func (r wechatAccessTokenResp) UnionID() string { - unionid, ok := r["unionid"].(string) - if ok { - return unionid - } - return "" -} - -type wechatUserInfoResp map[string]interface{} - -func (r wechatUserInfoResp) OpenID() string { - openid, ok := r["openid"].(string) - if ok { - return openid - } - return "" -} - -func wechatFetchAccessTokenResp( - client OAuthHTTPClient, - code string, - appid string, - secret string, -) (r wechatAccessTokenResp, err error) { - v := url.Values{} - v.Set("grant_type", "authorization_code") - v.Add("code", code) - v.Add("appid", appid) - v.Add("secret", secret) - - resp, err := client.PostForm(wechatAccessTokenURL, v) - if resp != nil { - defer resp.Body.Close() - } - - if err != nil { - return - } - - // wechat always return 200 - // to know if there is error, we need to parse the response body - if resp.StatusCode != 200 { - err = fmt.Errorf("wechat: unexpected status code: %d", resp.StatusCode) - return - } - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return - } - - err = json.NewDecoder(bytes.NewReader(body)).Decode(&r) - if err != nil { - return - } - if r.AccessToken() == "" { - // failed to obtain access token, parse the error response - var errResp wechatOAuthErrorResp - err = json.NewDecoder(bytes.NewReader(body)).Decode(&errResp) - if err != nil { - return - } - err = errResp.AsError() - return - } - return -} - -func wechatFetchUserProfile( - client OAuthHTTPClient, - accessTokenResp wechatAccessTokenResp, -) (userProfile wechatUserInfoResp, err error) { - v := url.Values{} - v.Set("openid", accessTokenResp.OpenID()) - v.Set("access_token", accessTokenResp.AccessToken()) - v.Set("lang", "en") - - resp, err := client.PostForm(wechatUserInfoURL, v) - if resp != nil { - defer resp.Body.Close() - } - - if err != nil { - return - } - - // wechat always return 200 - // to know if there is error, we need to parse the response body - if resp.StatusCode != 200 { - err = fmt.Errorf("wechat: unexpected status code: %d", resp.StatusCode) - return - } - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return - } - - err = json.NewDecoder(bytes.NewReader(body)).Decode(&userProfile) - if err != nil { - return - } - if userProfile.OpenID() == "" { - // failed to obtain id from user info, parse the error response - var errResp wechatOAuthErrorResp - err = json.NewDecoder(bytes.NewReader(body)).Decode(&errResp) - if err != nil { - return - } - err = errResp.AsError() - return - } - - return -} diff --git a/pkg/lib/authn/sso/wechat_test.go b/pkg/lib/authn/sso/wechat_test.go deleted file mode 100644 index fc0aa72cc0..0000000000 --- a/pkg/lib/authn/sso/wechat_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package sso - -import ( - "testing" - - "github.com/authgear/authgear-server/pkg/lib/config" - . "github.com/smartystreets/goconvey/convey" -) - -func TestWechatImpl(t *testing.T) { - Convey("WechatImpl", t, func() { - - g := &WechatImpl{ - ProviderConfig: config.OAuthSSOProviderConfig{ - ClientID: "client_id", - Type: config.OAuthSSOProviderTypeWechat, - }, - HTTPClient: OAuthHTTPClient{}, - } - - u, err := g.GetAuthURL(GetAuthURLParam{ - Nonce: "nonce", - State: "state", - Prompt: []string{"login"}, - }) - So(err, ShouldBeNil) - So(u, ShouldEqual, "https://open.weixin.qq.com/connect/oauth2/authorize?appid=client_id&redirect_uri=&response_type=code&scope=snsapi_userinfo&state=state") - }) -} diff --git a/pkg/lib/authn/stdattrs/normalizer.go b/pkg/lib/authn/stdattrs/normalizer.go index b093799130..49154c045f 100644 --- a/pkg/lib/authn/stdattrs/normalizer.go +++ b/pkg/lib/authn/stdattrs/normalizer.go @@ -3,8 +3,8 @@ package stdattrs import ( "golang.org/x/text/language" + "github.com/authgear/authgear-server/pkg/api/internalinterface" "github.com/authgear/authgear-server/pkg/api/model" - "github.com/authgear/authgear-server/pkg/lib/authn/identity/loginid" "github.com/authgear/authgear-server/pkg/util/phone" "github.com/authgear/authgear-server/pkg/util/validation" ) @@ -12,7 +12,7 @@ import ( //go:generate mockgen -source=normalizer.go -destination=normalizer_mock_test.go -package stdattrs type LoginIDNormalizerFactory interface { - NormalizerWithLoginIDType(loginIDKeyType model.LoginIDKeyType) loginid.Normalizer + NormalizerWithLoginIDType(loginIDKeyType model.LoginIDKeyType) internalinterface.LoginIDNormalizer } type Normalizer struct { diff --git a/pkg/lib/authn/stdattrs/normalizer_mock_test.go b/pkg/lib/authn/stdattrs/normalizer_mock_test.go index 95f9bf55ab..20a44156c2 100644 --- a/pkg/lib/authn/stdattrs/normalizer_mock_test.go +++ b/pkg/lib/authn/stdattrs/normalizer_mock_test.go @@ -7,8 +7,8 @@ package stdattrs import ( reflect "reflect" + internalinterface "github.com/authgear/authgear-server/pkg/api/internalinterface" model "github.com/authgear/authgear-server/pkg/api/model" - loginid "github.com/authgear/authgear-server/pkg/lib/authn/identity/loginid" gomock "github.com/golang/mock/gomock" ) @@ -36,10 +36,10 @@ func (m *MockLoginIDNormalizerFactory) EXPECT() *MockLoginIDNormalizerFactoryMoc } // NormalizerWithLoginIDType mocks base method. -func (m *MockLoginIDNormalizerFactory) NormalizerWithLoginIDType(loginIDKeyType model.LoginIDKeyType) loginid.Normalizer { +func (m *MockLoginIDNormalizerFactory) NormalizerWithLoginIDType(loginIDKeyType model.LoginIDKeyType) internalinterface.LoginIDNormalizer { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NormalizerWithLoginIDType", loginIDKeyType) - ret0, _ := ret[0].(loginid.Normalizer) + ret0, _ := ret[0].(internalinterface.LoginIDNormalizer) return ret0 } diff --git a/pkg/lib/cloudstorage/azure.go b/pkg/lib/cloudstorage/azure.go index f000599508..11e2857b83 100644 --- a/pkg/lib/cloudstorage/azure.go +++ b/pkg/lib/cloudstorage/azure.go @@ -7,6 +7,7 @@ import ( "time" "github.com/Azure/azure-storage-blob-go/azblob" + "github.com/authgear/authgear-server/pkg/util/clock" ) diff --git a/pkg/lib/cloudstorage/gcs.go b/pkg/lib/cloudstorage/gcs.go index 083affa5f2..a9157300b3 100644 --- a/pkg/lib/cloudstorage/gcs.go +++ b/pkg/lib/cloudstorage/gcs.go @@ -10,9 +10,10 @@ import ( "strings" "cloud.google.com/go/storage" - "github.com/authgear/authgear-server/pkg/util/clock" "google.golang.org/api/option" raw "google.golang.org/api/storage/v1" + + "github.com/authgear/authgear-server/pkg/util/clock" ) type GCSStorage struct { diff --git a/pkg/lib/config/config.go b/pkg/lib/config/config.go index d409fae7cd..071b143db3 100644 --- a/pkg/lib/config/config.go +++ b/pkg/lib/config/config.go @@ -8,6 +8,7 @@ import ( "sigs.k8s.io/yaml" "github.com/authgear/authgear-server/pkg/api/model" + liboauthrelyingparty "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty" "github.com/authgear/authgear-server/pkg/util/validation" ) @@ -101,7 +102,7 @@ func (c *AppConfig) Validate(ctx *validation.Context) { // Validation 1: lifetime of refresh token >= lifetime of access token c.validateTokenLifetime(ctx) - // Validation 2: OAuth provider cannot duplicate. + // Validation 2: oauth provider c.validateOAuthProvider(ctx) // Validation 3: identity must have usable primary authenticator. @@ -137,36 +138,26 @@ func (c *AppConfig) validateTokenLifetime(ctx *validation.Context) { } func (c *AppConfig) validateOAuthProvider(ctx *validation.Context) { - oAuthProviderIDs := map[string]struct{}{} + // We used to validate that ProviderID is unique. + // We now relax the validation, only alias is unique. oauthProviderAliases := map[string]struct{}{} - for i, provider := range c.Identity.OAuth.Providers { - // Ensure provider ID is not duplicated - // Except WeChat provider with different app type - providerID := map[string]interface{}{} - for k, v := range provider.ProviderID().Claims() { - providerID[k] = v - } - if provider.Type == OAuthSSOProviderTypeWechat { - providerID["app_type"] = provider.AppType - } - id, err := json.Marshal(providerID) - if err != nil { - panic("config: cannot marshal provider ID claims: " + err.Error()) - } - if _, ok := oAuthProviderIDs[string(id)]; ok { - ctx.Child("identity", "oauth", "providers", strconv.Itoa(i)). - EmitErrorMessage("duplicated OAuth provider") + for i, providerConfig := range c.Identity.OAuth.Providers { + // We used to ensure provider ID is not duplicated. + // We now expect alias to be unique. + alias := providerConfig.Alias() + childCtx := ctx.Child("identity", "oauth", "providers", strconv.Itoa(i)) + + if _, ok := oauthProviderAliases[alias]; ok { + childCtx.EmitErrorMessage("duplicated OAuth provider alias") continue } - oAuthProviderIDs[string(id)] = struct{}{} + oauthProviderAliases[alias] = struct{}{} - // Ensure alias is not duplicated. - if _, ok := oauthProviderAliases[provider.Alias]; ok { - ctx.Child("identity", "oauth", "providers", strconv.Itoa(i)). - EmitErrorMessage("duplicated OAuth provider alias") - continue + // Validate provider config if it is a builin provider. + provider := providerConfig.MustGetProvider() + if builtinProvider, ok := provider.(liboauthrelyingparty.BuiltinProvider); ok { + builtinProvider.ValidateProviderConfig(childCtx, providerConfig) } - oauthProviderAliases[provider.Alias] = struct{}{} } } diff --git a/pkg/lib/config/config_test.go b/pkg/lib/config/config_test.go index c2ad27ffa0..9207a7dbf3 100644 --- a/pkg/lib/config/config_test.go +++ b/pkg/lib/config/config_test.go @@ -11,6 +11,14 @@ import ( "sigs.k8s.io/yaml" "github.com/authgear/authgear-server/pkg/lib/config" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/adfs" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/apple" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/azureadb2c" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/azureadv2" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/facebook" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/google" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/linkedin" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/wechat" ) func TestAppConfig(t *testing.T) { diff --git a/pkg/lib/config/configsource/resources_test.go b/pkg/lib/config/configsource/resources_test.go index 0b6086a414..8910cb9ced 100644 --- a/pkg/lib/config/configsource/resources_test.go +++ b/pkg/lib/config/configsource/resources_test.go @@ -10,6 +10,8 @@ import ( apimodel "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/config" configtest "github.com/authgear/authgear-server/pkg/lib/config/test" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/facebook" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/google" "github.com/authgear/authgear-server/pkg/util/resource" ) diff --git a/pkg/lib/config/feature_identity.go b/pkg/lib/config/feature_identity.go index 7a20ea7539..b5f1fe985e 100644 --- a/pkg/lib/config/feature_identity.go +++ b/pkg/lib/config/feature_identity.go @@ -1,5 +1,11 @@ package config +import ( + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + + liboauthrelyingparty "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty" +) + var _ = FeatureConfigSchema.Add("IdentityFeatureConfig", ` { "type": "object", @@ -112,6 +118,32 @@ type OAuthSSOProvidersFeatureConfig struct { Wechat *OAuthSSOProviderFeatureConfig `json:"wechat,omitempty"` } +func (c *OAuthSSOProvidersFeatureConfig) IsDisabled(cfg oauthrelyingparty.ProviderConfig) bool { + switch cfg.Type() { + case liboauthrelyingparty.TypeGoogle: + return c.Google.Disabled + case liboauthrelyingparty.TypeFacebook: + return c.Facebook.Disabled + case liboauthrelyingparty.TypeGithub: + return c.Github.Disabled + case liboauthrelyingparty.TypeLinkedin: + return c.LinkedIn.Disabled + case liboauthrelyingparty.TypeAzureADv2: + return c.Azureadv2.Disabled + case liboauthrelyingparty.TypeAzureADB2C: + return c.Azureadb2c.Disabled + case liboauthrelyingparty.TypeADFS: + return c.ADFS.Disabled + case liboauthrelyingparty.TypeApple: + return c.Apple.Disabled + case liboauthrelyingparty.TypeWechat: + return c.Wechat.Disabled + default: + // Not a provider we recognize here. Allow it. + return false + } +} + var _ = FeatureConfigSchema.Add("OAuthSSOProviderFeatureConfig", ` { "type": "object", diff --git a/pkg/lib/config/feature_test.go b/pkg/lib/config/feature_test.go index 138a667ef3..eeed479a00 100644 --- a/pkg/lib/config/feature_test.go +++ b/pkg/lib/config/feature_test.go @@ -7,10 +7,11 @@ import ( "os" "testing" - "github.com/authgear/authgear-server/pkg/lib/config" . "github.com/smartystreets/goconvey/convey" goyaml "gopkg.in/yaml.v2" "sigs.k8s.io/yaml" + + "github.com/authgear/authgear-server/pkg/lib/config" ) func TestParseFeatureConfig(t *testing.T) { diff --git a/pkg/lib/config/identity.go b/pkg/lib/config/identity.go index bc7e92ddd1..a50660b507 100644 --- a/pkg/lib/config/identity.go +++ b/pkg/lib/config/identity.go @@ -1,8 +1,7 @@ package config import ( - "fmt" - "strconv" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" "github.com/authgear/authgear-server/pkg/api/model" ) @@ -250,336 +249,25 @@ var _ = Schema.Add("OAuthSSOConfig", ` "type": "object", "additionalProperties": false, "properties": { - "providers": { "type": "array", "items": { "$ref": "#/$defs/OAuthSSOProviderConfig" } } + "providers": { "type": "array", "items": { "type": "object" } } } } `) type OAuthSSOConfig struct { - Providers []OAuthSSOProviderConfig `json:"providers,omitempty"` + Providers []oauthrelyingparty.ProviderConfig `json:"providers,omitempty"` } -func (c *OAuthSSOConfig) GetProviderConfig(alias string) (*OAuthSSOProviderConfig, bool) { +func (c *OAuthSSOConfig) GetProviderConfig(alias string) (oauthrelyingparty.ProviderConfig, bool) { for _, conf := range c.Providers { - if conf.Alias == alias { + if conf.Alias() == alias { cc := conf - return &cc, true + return cc, true } } return nil, false } -var _ = Schema.Add("OAuthSSOProviderType", ` -{ - "type": "string", - "enum": [ - "google", - "facebook", - "github", - "linkedin", - "azureadv2", - "azureadb2c", - "adfs", - "apple", - "wechat" - ] -} -`) - -type OAuthSSOProviderType string - -func (t OAuthSSOProviderType) Scope() []string { - switch t { - case OAuthSSOProviderTypeGoogle: - // https://developers.google.com/identity/protocols/oauth2/openid-connect - return []string{"openid", "profile", "email"} - case OAuthSSOProviderTypeFacebook: - // https://developers.facebook.com/docs/permissions/reference - return []string{"email", "public_profile"} - case OAuthSSOProviderTypeGithub: - // https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps - return []string{"read:user", "user:email"} - case OAuthSSOProviderTypeLinkedIn: - // https://docs.microsoft.com/en-us/linkedin/shared/references/v2/profile/lite-profile - // https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/primary-contact-api?context=linkedin/compliance/context - return []string{"r_liteprofile", "r_emailaddress"} - case OAuthSSOProviderTypeAzureADv2: - // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes - return []string{"openid", "profile", "email"} - case OAuthSSOProviderTypeAzureADB2C: - // Instead of specifying scope to request a specific claim, - // the developer must customize the policy to allow which claims are returned to the relying party. - // If the developer is using User Flow policy, then those claims are called Application Claims. - return []string{"openid"} - case OAuthSSOProviderTypeADFS: - // The supported scopes are observed from a AD FS server. - return []string{"openid", "profile", "email"} - case OAuthSSOProviderTypeApple: - // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms - return []string{"name", "email"} - case OAuthSSOProviderTypeWechat: - // https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html - return []string{"snsapi_userinfo"} - } - - panic(fmt.Sprintf("oauth: unknown provider type %s", string(t))) -} - -func (t OAuthSSOProviderType) EmailRequired() bool { - switch t { - case OAuthSSOProviderTypeGoogle: - return true - case OAuthSSOProviderTypeFacebook: - return true - case OAuthSSOProviderTypeGithub: - return true - case OAuthSSOProviderTypeLinkedIn: - return true - case OAuthSSOProviderTypeAzureADv2: - return true - case OAuthSSOProviderTypeAzureADB2C: - return true - case OAuthSSOProviderTypeADFS: - return true - case OAuthSSOProviderTypeApple: - return true - case OAuthSSOProviderTypeWechat: - return false - } - panic(fmt.Sprintf("oauth: unknown provider type %s", string(t))) -} - -const ( - OAuthSSOProviderTypeGoogle OAuthSSOProviderType = "google" - OAuthSSOProviderTypeFacebook OAuthSSOProviderType = "facebook" - OAuthSSOProviderTypeGithub OAuthSSOProviderType = "github" - OAuthSSOProviderTypeLinkedIn OAuthSSOProviderType = "linkedin" - OAuthSSOProviderTypeAzureADv2 OAuthSSOProviderType = "azureadv2" - OAuthSSOProviderTypeAzureADB2C OAuthSSOProviderType = "azureadb2c" - OAuthSSOProviderTypeADFS OAuthSSOProviderType = "adfs" - OAuthSSOProviderTypeApple OAuthSSOProviderType = "apple" - OAuthSSOProviderTypeWechat OAuthSSOProviderType = "wechat" -) - -var OAuthSSOProviderTypes = []OAuthSSOProviderType{ - OAuthSSOProviderTypeGoogle, - OAuthSSOProviderTypeFacebook, - OAuthSSOProviderTypeGithub, - OAuthSSOProviderTypeLinkedIn, - OAuthSSOProviderTypeAzureADv2, - OAuthSSOProviderTypeAzureADB2C, - OAuthSSOProviderTypeADFS, - OAuthSSOProviderTypeApple, - OAuthSSOProviderTypeWechat, -} - -var _ = Schema.Add("OAuthSSOWeChatAppType", ` -{ - "type": "string", - "enum": [ - "mobile", - "web" - ] -} -`) - -type OAuthSSOWeChatAppType string - -const ( - OAuthSSOWeChatAppTypeWeb OAuthSSOWeChatAppType = "web" - OAuthSSOWeChatAppTypeMobile OAuthSSOWeChatAppType = "mobile" -) - -var _ = Schema.Add("OAuthSSOProviderConfig", ` -{ - "type": "object", - "additionalProperties": false, - "properties": { - "alias": { "type": "string" }, - "type": { "$ref": "#/$defs/OAuthSSOProviderType" }, - "modify_disabled": { "type": "boolean" }, - "client_id": { "type": "string", "minLength": 1 }, - "claims": { "$ref": "#/$defs/OAuthClaimsConfig" }, - "tenant": { "type": "string" }, - "policy": { "type": "string" }, - "key_id": { "type": "string" }, - "team_id": { "type": "string" }, - "app_type": { "$ref": "#/$defs/OAuthSSOWeChatAppType" }, - "account_id": { "type": "string", "format": "wechat_account_id"}, - "is_sandbox_account": { "type": "boolean" }, - "wechat_redirect_uris": { "type": "array", "items": { "type": "string", "format": "uri" } }, - "discovery_document_endpoint": { "type": "string", "format": "uri" } - }, - "required": ["alias", "type", "client_id"], - "allOf": [ - { - "if": { "properties": { "type": { "const": "apple" } } }, - "then": { - "required": ["key_id", "team_id"] - } - }, - { - "if": { "properties": { "type": { "const": "azureadv2" } } }, - "then": { - "required": ["tenant"] - } - }, - { - "if": { "properties": { "type": { "const": "wechat" } } }, - "then": { - "required": ["app_type", "account_id"] - } - }, - { - "if": { "properties": { "type": { "const": "adfs" } } }, - "then": { - "required": ["discovery_document_endpoint"] - } - }, - { - "if": { "properties": { "type": { "const": "azureadb2c" } } }, - "then": { - "required": ["tenant", "policy"] - } - } - ] -} -`) - -type OAuthSSOProviderConfig struct { - Alias string `json:"alias,omitempty"` - Type OAuthSSOProviderType `json:"type,omitempty"` - ModifyDisabled *bool `json:"modify_disabled,omitempty"` - ClientID string `json:"client_id"` - Claims *OAuthClaimsConfig `json:"claims,omitempty"` - - // Tenant is specific to `azureadv2` and `azureadb2c` - Tenant string `json:"tenant,omitempty"` - - // Policy is specific to `azureadb2c` - Policy string `json:"policy,omitempty"` - - // KeyID and TeamID are specific to `apple` - KeyID string `json:"key_id,omitempty"` - TeamID string `json:"team_id,omitempty"` - - // AppType is specific to `wechat`, support web or mobile - AppType OAuthSSOWeChatAppType `json:"app_type,omitempty"` - AccountID string `json:"account_id,omitempty"` - IsSandboxAccount bool `json:"is_sandbox_account,omitempty"` - WeChatRedirectURIs []string `json:"wechat_redirect_uris,omitempty"` - - // DiscoveryDocumentEndpoint is specific to `adfs`. - DiscoveryDocumentEndpoint string `json:"discovery_document_endpoint,omitempty"` -} - -func (c *OAuthSSOProviderConfig) SetDefaults() { - if c.ModifyDisabled == nil { - c.ModifyDisabled = newBool(false) - } - - if c.Claims.Email.Required == nil { - emailRequired := c.Type.EmailRequired() - c.Claims.Email.Required = &emailRequired - } -} - -func (c *OAuthSSOProviderConfig) ProviderID() ProviderID { - keys := map[string]interface{}{} - switch c.Type { - case OAuthSSOProviderTypeGoogle: - // Google supports OIDC. - // sub is public, not scoped to anything so changing client_id does not affect sub. - // Therefore, ProviderID is simply Type. - // - // Rotating the OAuth application is OK. - break - case OAuthSSOProviderTypeFacebook: - // Facebook does NOT support OIDC. - // Facebook user ID is scoped to client_id. - // Therefore, ProviderID is Type + client_id. - // - // Rotating the OAuth application is problematic. - // But if email remains unchanged, the user can associate their account. - keys["client_id"] = c.ClientID - case OAuthSSOProviderTypeGithub: - // Github does NOT support OIDC. - // Github user ID is public, not scoped to anything. - break - case OAuthSSOProviderTypeLinkedIn: - // LinkedIn is the same as Facebook. - keys["client_id"] = c.ClientID - case OAuthSSOProviderTypeAzureADv2: - // Azure AD v2 supports OIDC. - // sub is pairwise and is scoped to client_id. - // However, oid is powerful alternative to sub. - // oid is also pairwise and is scoped to tenant. - // We use oid as ProviderSubjectID so ProviderID is Type + tenant. - // - // Rotating the OAuth application is OK. - // But rotating the tenant is problematic. - // But if email remains unchanged, the user can associate their account. - keys["tenant"] = c.Tenant - case OAuthSSOProviderTypeAzureADB2C: - // By default sub is the Object ID of the user in the directory. - // A tenant is a directory. - // sub is scoped to the tenant only. - // Therefore, ProviderID is Type + tenant. - // - // See https://docs.microsoft.com/en-us/azure/active-directory-b2c/tokens-overview#claims - keys["tenant"] = c.Tenant - case OAuthSSOProviderTypeApple: - // Apple supports OIDC. - // sub is pairwise and is scoped to team_id. - // Therefore, ProviderID is Type + team_id. - // - // Rotating the OAuth application is OK. - // But rotating the Apple Developer account is problematic. - // Since Apple has private relay to hide the real email, - // the user may not be associate their account. - keys["team_id"] = c.TeamID - case OAuthSSOProviderTypeWechat: - // WeChat does NOT support OIDC. - // In the same Weixin Open Platform account, the user UnionID is unique. - // The id is scoped to Open Platform account. - // https://developers.weixin.qq.com/miniprogram/en/dev/framework/open-ability/union-id.html - keys["account_id"] = c.AccountID - keys["is_sandbox_account"] = strconv.FormatBool(c.IsSandboxAccount) - } - - return ProviderID{ - Type: string(c.Type), - Keys: keys, - } -} - -// ProviderID combining with a subject ID identifies an user from an external system. -type ProviderID struct { - Type string - Keys map[string]interface{} -} - -func (p ProviderID) Claims() map[string]interface{} { - claim := map[string]interface{}{} - claim["type"] = p.Type - for k, v := range p.Keys { - claim[k] = v - } - return claim -} - -func (p ProviderID) Equal(that *ProviderID) bool { - if p.Type != that.Type || len(p.Keys) != len(that.Keys) { - return false - } - for k, v := range p.Keys { - if tv, ok := that.Keys[k]; !ok || tv != v { - return false - } - } - return true -} - var _ = Schema.Add("PromotionConflictBehavior", ` { "type": "string", @@ -633,40 +321,3 @@ func (c *BiometricConfig) SetDefaults() { c.ListEnabled = newBool(false) } } - -var _ = Schema.Add("OAuthClaimsConfig", ` -{ - "type": "object", - "additionalProperties": false, - "properties": { - "email": { "$ref": "#/$defs/OAuthClaimConfig" } - } -} -`) - -type OAuthClaimsConfig struct { - Email *OAuthClaimConfig `json:"email,omitempty"` -} - -var _ = Schema.Add("OAuthClaimConfig", ` -{ - "type": "object", - "additionalProperties": false, - "properties": { - "assume_verified": { "type": "boolean" }, - "required": { "type": "boolean" } - } -} -`) - -type OAuthClaimConfig struct { - AssumeVerified *bool `json:"assume_verified,omitempty"` - Required *bool `json:"required,omitempty"` -} - -func (c *OAuthClaimConfig) SetDefaults() { - if c.AssumeVerified == nil { - c.AssumeVerified = newBool(true) - } - // Required is type-specific so the default is not set here. -} diff --git a/pkg/lib/config/rate_limits_env_test.go b/pkg/lib/config/rate_limits_env_test.go index ed59ce698b..238357164b 100644 --- a/pkg/lib/config/rate_limits_env_test.go +++ b/pkg/lib/config/rate_limits_env_test.go @@ -4,8 +4,9 @@ import ( "testing" "time" - "github.com/authgear/authgear-server/pkg/lib/config" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/authgear-server/pkg/lib/config" ) func TestParseRateLimitsEnv(t *testing.T) { diff --git a/pkg/lib/config/secret.go b/pkg/lib/config/secret.go index b737ab6cda..66921d8697 100644 --- a/pkg/lib/config/secret.go +++ b/pkg/lib/config/secret.go @@ -149,18 +149,19 @@ func (c *SecretConfig) validateOAuthProviders(ctx *validation.Context, appConfig oauth, ok := data.(*OAuthSSOProviderCredentials) if ok { for _, p := range appConfig.Identity.OAuth.Providers { + providerAlias := p.Alias() var matchedItem *OAuthSSOProviderCredentialsItem = nil var matchedItemIndex int = -1 for index := range oauth.Items { item := oauth.Items[index] - if p.Alias == item.Alias { + if providerAlias == item.Alias { matchedItem = &item matchedItemIndex = index break } } if matchedItem == nil { - ctx.EmitErrorMessage(fmt.Sprintf("OAuth SSO provider client credentials for '%s' is required", p.Alias)) + ctx.EmitErrorMessage(fmt.Sprintf("OAuth SSO provider client credentials for '%s' is required", providerAlias)) } else { if matchedItem.ClientSecret == "" { ctx.Child("secrets", fmt.Sprintf("%d", secretIndex), "data", "items", fmt.Sprintf("%d", matchedItemIndex)).EmitError( diff --git a/pkg/lib/config/secret_data.go b/pkg/lib/config/secret_data.go index a0a74aa8b0..64fb631357 100644 --- a/pkg/lib/config/secret_data.go +++ b/pkg/lib/config/secret_data.go @@ -3,9 +3,10 @@ package config import ( "encoding/json" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/authgear/authgear-server/pkg/util/jwkutil" "github.com/authgear/authgear-server/pkg/util/slice" - "github.com/lestrrat-go/jwx/v2/jwk" ) var _ = SecretConfigSchema.Add("DatabaseCredentials", ` diff --git a/pkg/lib/config/secret_test.go b/pkg/lib/config/secret_test.go index 0f77059635..bc12beed7d 100644 --- a/pkg/lib/config/secret_test.go +++ b/pkg/lib/config/secret_test.go @@ -10,6 +10,7 @@ import ( goyaml "gopkg.in/yaml.v2" "github.com/authgear/authgear-server/pkg/lib/config" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/google" ) func TestParseSecret(t *testing.T) { diff --git a/pkg/lib/config/secret_update_instruction_context.go b/pkg/lib/config/secret_update_instruction_context.go index c2894982f1..4faab5e801 100644 --- a/pkg/lib/config/secret_update_instruction_context.go +++ b/pkg/lib/config/secret_update_instruction_context.go @@ -4,8 +4,9 @@ import ( mathrand "math/rand" "time" - "github.com/authgear/authgear-server/pkg/util/clock" "github.com/lestrrat-go/jwx/v2/jwk" + + "github.com/authgear/authgear-server/pkg/util/clock" ) type SecretConfigUpdateInstructionContext struct { diff --git a/pkg/lib/config/secret_update_instruction_test.go b/pkg/lib/config/secret_update_instruction_test.go index 4253e744a5..6b5f31c421 100644 --- a/pkg/lib/config/secret_update_instruction_test.go +++ b/pkg/lib/config/secret_update_instruction_test.go @@ -13,11 +13,12 @@ import ( goyaml "gopkg.in/yaml.v2" "sigs.k8s.io/yaml" + "github.com/lestrrat-go/jwx/v2/jwk" + . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/util/clock" "github.com/authgear/authgear-server/pkg/util/jwkutil" - "github.com/lestrrat-go/jwx/v2/jwk" - . "github.com/smartystreets/goconvey/convey" ) func TestSecretConfigUpdateInstruction(t *testing.T) { diff --git a/pkg/lib/config/testdata/config_tests.yaml b/pkg/lib/config/testdata/config_tests.yaml index c8676cc640..02c1b2824d 100644 --- a/pkg/lib/config/testdata/config_tests.yaml +++ b/pkg/lib/config/testdata/config_tests.yaml @@ -209,68 +209,6 @@ config: x_application_type: confidential redirect_uris: - "http://example.com/oauth-redirect" ---- -name: dupe-oauth-provider -error: |- - invalid configuration: - /identity/oauth/providers/1: duplicated OAuth provider -config: - id: test - http: - public_origin: http://test - identity: - oauth: - providers: - - alias: google_a - type: google - client_id: client_a - - alias: google_b - type: google - client_id: client_b - ---- -name: dupe-wechat-oauth-provider -error: |- - invalid configuration: - /identity/oauth/providers/1: duplicated OAuth provider -config: - id: test - http: - public_origin: http://test - identity: - oauth: - providers: - - alias: wechat_a - type: wechat - client_id: client_id_a - account_id: gh_accountid - app_type: mobile - - alias: wechat_b - type: wechat - client_id: client_id_b - account_id: gh_accountid - app_type: mobile - ---- -name: dupe-wechat-oauth-provider-different-app-type -error: null -config: - id: test - http: - public_origin: http://test - identity: - oauth: - providers: - - alias: wechat_a - type: wechat - client_id: client_id_a - account_id: gh_accountida - app_type: mobile - - alias: wechat_b - type: wechat - client_id: client_id_b - account_id: gh_accountidb - app_type: web --- name: invalid-wechat-oauth-provider-account-id @@ -296,7 +234,7 @@ name: missing-oauth-provider-alias error: |- invalid configuration: /identity/oauth/providers/0: required - map[actual:[client_id type] expected:[alias client_id type] missing:[alias]] + map[actual:[claims client_id modify_disabled type] expected:[alias client_id type] missing:[alias]] config: id: test http: @@ -331,7 +269,7 @@ name: oauth-provider-apple error: |- invalid configuration: /identity/oauth/providers/0: required - map[actual:[alias client_id type] expected:[key_id team_id] missing:[key_id team_id]] + map[actual:[alias claims client_id modify_disabled type] expected:[alias client_id key_id team_id type] missing:[key_id team_id]] config: id: test http: @@ -348,7 +286,7 @@ name: oauth-provider-azureadv2 error: |- invalid configuration: /identity/oauth/providers/0: required - map[actual:[alias client_id type] expected:[tenant] missing:[tenant]] + map[actual:[alias claims client_id modify_disabled type] expected:[alias client_id tenant type] missing:[tenant]] config: id: test http: @@ -364,7 +302,7 @@ name: oauth-provider-azureadb2c error: |- invalid configuration: /identity/oauth/providers/0: required - map[actual:[alias client_id type] expected:[policy tenant] missing:[policy tenant]] + map[actual:[alias claims client_id modify_disabled type] expected:[alias client_id policy tenant type] missing:[policy tenant]] config: id: test http: @@ -381,7 +319,7 @@ name: oauth-provider-adfs error: |- invalid configuration: /identity/oauth/providers/0: required - map[actual:[alias client_id type] expected:[discovery_document_endpoint] missing:[discovery_document_endpoint]] + map[actual:[alias claims client_id modify_disabled type] expected:[alias client_id discovery_document_endpoint type] missing:[discovery_document_endpoint]] config: id: test http: diff --git a/pkg/lib/db/util/dump.go b/pkg/lib/db/util/dump.go index 8871d82e84..3dc28ab3bf 100644 --- a/pkg/lib/db/util/dump.go +++ b/pkg/lib/db/util/dump.go @@ -9,9 +9,10 @@ import ( "path/filepath" "time" + "github.com/lib/pq" + "github.com/authgear/authgear-server/pkg/lib/infra/db" "github.com/authgear/authgear-server/pkg/util/log" - "github.com/lib/pq" ) type Dumper struct { diff --git a/pkg/lib/facade/coordinator.go b/pkg/lib/facade/coordinator.go index 99a130d404..7a382d6bb6 100644 --- a/pkg/lib/facade/coordinator.go +++ b/pkg/lib/facade/coordinator.go @@ -3,6 +3,9 @@ package facade import ( "errors" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/authgear-server/pkg/api" "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/api/event" @@ -21,7 +24,6 @@ import ( "github.com/authgear/authgear-server/pkg/util/accesscontrol" "github.com/authgear/authgear-server/pkg/util/clock" "github.com/authgear/authgear-server/pkg/util/errorutil" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) type EventService interface { @@ -588,11 +590,11 @@ func (c *Coordinator) markOAuthEmailAsVerified(info *identity.Info) error { providerID := info.OAuth.ProviderID - var cfg *config.OAuthSSOProviderConfig + var cfg oauthrelyingparty.ProviderConfig for _, c := range c.IdentityConfig.OAuth.Providers { - if c.ProviderID().Equal(&providerID) { - c := c - cfg = &c + c := c + if c.ProviderID().Equal(providerID) { + cfg = c break } } @@ -600,13 +602,16 @@ func (c *Coordinator) markOAuthEmailAsVerified(info *identity.Info) error { standardClaims := info.IdentityAwareStandardClaims() email, ok := standardClaims[model.ClaimEmail] - if ok && cfg != nil && *cfg.Claims.Email.AssumeVerified { - // Mark as verified if OAuth email is assumed to be verified - err := c.markVerified(info.UserID, map[model.ClaimName]string{ - model.ClaimEmail: email, - }) - if err != nil { - return err + if ok && cfg != nil { + assumedVerified := cfg.EmailClaimConfig().AssumeVerified() + if assumedVerified { + // Mark as verified if OAuth email is assumed to be verified + err := c.markVerified(info.UserID, map[model.ClaimName]string{ + model.ClaimEmail: email, + }) + if err != nil { + return err + } } } diff --git a/pkg/lib/facade/identity.go b/pkg/lib/facade/identity.go index 1496d5815d..738dd1817a 100644 --- a/pkg/lib/facade/identity.go +++ b/pkg/lib/facade/identity.go @@ -1,8 +1,9 @@ package facade import ( - "github.com/authgear/authgear-server/pkg/lib/authn/identity" "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + + "github.com/authgear/authgear-server/pkg/lib/authn/identity" ) type IdentityFacade struct { diff --git a/pkg/lib/feature/forgotpassword/service.go b/pkg/lib/feature/forgotpassword/service.go index bb889da18c..ea740b7c21 100644 --- a/pkg/lib/feature/forgotpassword/service.go +++ b/pkg/lib/feature/forgotpassword/service.go @@ -4,6 +4,8 @@ import ( "errors" "strings" + "github.com/iawaknahc/jsonschema/pkg/jsonpointer" + "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/authn/authenticator" @@ -14,7 +16,6 @@ import ( "github.com/authgear/authgear-server/pkg/lib/ratelimit" "github.com/authgear/authgear-server/pkg/util/errorutil" "github.com/authgear/authgear-server/pkg/util/log" - "github.com/iawaknahc/jsonschema/pkg/jsonpointer" ) type Logger struct{ *log.Logger } diff --git a/pkg/lib/feature/siwe/service.go b/pkg/lib/feature/siwe/service.go index 58a3975bce..c827270e3c 100644 --- a/pkg/lib/feature/siwe/service.go +++ b/pkg/lib/feature/siwe/service.go @@ -8,6 +8,8 @@ import ( "strconv" "time" + siwego "github.com/spruceid/siwe-go" + "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/config" @@ -18,7 +20,6 @@ import ( "github.com/authgear/authgear-server/pkg/util/log" "github.com/authgear/authgear-server/pkg/util/rand" "github.com/authgear/authgear-server/pkg/util/web3" - siwego "github.com/spruceid/siwe-go" ) //go:generate mockgen -source=service.go -destination=service_mock_test.go -package siwe diff --git a/pkg/lib/feature/stdattrs/transformer_test.go b/pkg/lib/feature/stdattrs/transformer_test.go index d2515a118a..d983dbda57 100644 --- a/pkg/lib/feature/stdattrs/transformer_test.go +++ b/pkg/lib/feature/stdattrs/transformer_test.go @@ -3,10 +3,11 @@ package stdattrs import ( "testing" + . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/util/httputil" - . "github.com/smartystreets/goconvey/convey" ) func TestPictureTransformer(t *testing.T) { diff --git a/pkg/lib/images/model_test.go b/pkg/lib/images/model_test.go index d3861e4fcb..94017248fa 100644 --- a/pkg/lib/images/model_test.go +++ b/pkg/lib/images/model_test.go @@ -3,8 +3,9 @@ package images_test import ( "testing" - "github.com/authgear/authgear-server/pkg/lib/images" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/authgear-server/pkg/lib/images" ) func TestFileMetadata(t *testing.T) { diff --git a/pkg/lib/infra/whatsapp/service.go b/pkg/lib/infra/whatsapp/service.go index f4b544fa6c..9a2e220d0d 100644 --- a/pkg/lib/infra/whatsapp/service.go +++ b/pkg/lib/infra/whatsapp/service.go @@ -6,10 +6,11 @@ import ( "fmt" "strings" + "github.com/sirupsen/logrus" + "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/util/intl" "github.com/authgear/authgear-server/pkg/util/log" - "github.com/sirupsen/logrus" ) type ServiceLogger struct{ *log.Logger } diff --git a/pkg/lib/interaction/context.go b/pkg/lib/interaction/context.go index 9acb8c97ae..71758d24df 100644 --- a/pkg/lib/interaction/context.go +++ b/pkg/lib/interaction/context.go @@ -5,6 +5,8 @@ import ( "net/url" "time" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/api/event" "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/authn/authenticationinfo" @@ -16,7 +18,6 @@ import ( "github.com/authgear/authgear-server/pkg/lib/authn/identity/biometric" "github.com/authgear/authgear-server/pkg/lib/authn/mfa" "github.com/authgear/authgear-server/pkg/lib/authn/otp" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/authn/user" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/facade" @@ -138,7 +139,9 @@ type SessionManager interface { } type OAuthProviderFactory interface { - NewOAuthProvider(alias string) sso.OAuthProvider + GetProviderConfig(alias string) (oauthrelyingparty.ProviderConfig, error) + GetAuthorizationURL(alias string, options oauthrelyingparty.GetAuthorizationURLOptions) (string, error) + GetUserProfile(alias string, options oauthrelyingparty.GetUserProfileOptions) (oauthrelyingparty.UserProfile, error) } type OAuthRedirectURIBuilder interface { diff --git a/pkg/lib/interaction/nodes/use_identity_oauth_provider.go b/pkg/lib/interaction/nodes/use_identity_oauth_provider.go index 842892a00b..13f732ce7c 100644 --- a/pkg/lib/interaction/nodes/use_identity_oauth_provider.go +++ b/pkg/lib/interaction/nodes/use_identity_oauth_provider.go @@ -3,11 +3,13 @@ package nodes import ( "net/url" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/api" "github.com/authgear/authgear-server/pkg/lib/authn/identity" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/interaction" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/wechat" "github.com/authgear/authgear-server/pkg/util/crypto" ) @@ -24,7 +26,7 @@ type InputUseIdentityOAuthProvider interface { type EdgeUseIdentityOAuthProvider struct { IsAuthentication bool IsCreating bool - Configs []config.OAuthSSOProviderConfig + Configs []oauthrelyingparty.ProviderConfig FeatureConfig *config.OAuthSSOProvidersFeatureConfig } @@ -32,8 +34,8 @@ func (e *EdgeUseIdentityOAuthProvider) GetIdentityCandidates() []identity.Candid candidates := []identity.Candidate{} for _, c := range e.Configs { conf := c - if !identity.IsOAuthSSOProviderTypeDisabled(conf.Type, e.FeatureConfig) { - candidates = append(candidates, identity.NewOAuthCandidate(&conf)) + if !identity.IsOAuthSSOProviderTypeDisabled(conf, e.FeatureConfig) { + candidates = append(candidates, identity.NewOAuthCandidate(conf)) } } return candidates @@ -46,14 +48,14 @@ func (e *EdgeUseIdentityOAuthProvider) Instantiate(ctx *interaction.Context, gra } alias := input.GetProviderAlias() - var oauthConfig *config.OAuthSSOProviderConfig + var oauthConfig oauthrelyingparty.ProviderConfig for _, c := range e.Configs { - if identity.IsOAuthSSOProviderTypeDisabled(c.Type, e.FeatureConfig) { + if identity.IsOAuthSSOProviderTypeDisabled(c, e.FeatureConfig) { continue } - if c.Alias == alias { + if c.Alias() == alias { conf := c - oauthConfig = &conf + oauthConfig = conf break } } @@ -64,34 +66,34 @@ func (e *EdgeUseIdentityOAuthProvider) Instantiate(ctx *interaction.Context, gra nonceSource := ctx.Nonces.GenerateAndSet() errorRedirectURI := input.GetErrorRedirectURI() - oauthProvider := ctx.OAuthProviderFactory.NewOAuthProvider(alias) - if oauthProvider == nil { - return nil, api.ErrOAuthProviderNotFound + providerConfig, err := ctx.OAuthProviderFactory.GetProviderConfig(alias) + if err != nil { + return nil, err } nonce := crypto.SHA256String(nonceSource) redirectURIForOAuthProvider := ctx.OAuthRedirectURIBuilder.SSOCallbackURL(alias).String() // Special case: wechat needs to use a special callback endpoint. - if oauthProvider.Config().Type == config.OAuthSSOProviderTypeWechat { + if providerConfig.Type() == wechat.Type { redirectURIForOAuthProvider = ctx.OAuthRedirectURIBuilder.WeChatCallbackEndpointURL().String() } - param := sso.GetAuthURLParam{ + param := oauthrelyingparty.GetAuthorizationURLOptions{ RedirectURI: redirectURIForOAuthProvider, // We use response_mode=form_post if it is supported. - ResponseMode: sso.ResponseModeFormPost, + ResponseMode: oauthrelyingparty.ResponseModeFormPost, Nonce: nonce, Prompt: input.GetPrompt(), State: ctx.WebSessionID, } - redirectURI, err := oauthProvider.GetAuthURL(param) + redirectURI, err := ctx.OAuthProviderFactory.GetAuthorizationURL(alias, param) if err != nil { return nil, err } // Special case: wechat needs to redirect a special page. - if oauthProvider.Config().Type == config.OAuthSSOProviderTypeWechat { + if providerConfig.Type() == wechat.Type { v := url.Values{} v.Add("x_auth_url", redirectURI) redirectURI = ctx.OAuthRedirectURIBuilder.WeChatAuthorizeURL(alias).String() + "?" + v.Encode() @@ -100,7 +102,7 @@ func (e *EdgeUseIdentityOAuthProvider) Instantiate(ctx *interaction.Context, gra return &NodeUseIdentityOAuthProvider{ IsAuthentication: e.IsAuthentication, IsCreating: e.IsCreating, - Config: *oauthConfig, + Config: oauthConfig, HashedNonce: nonce, ErrorRedirectURI: errorRedirectURI, RedirectURI: redirectURI, @@ -108,12 +110,12 @@ func (e *EdgeUseIdentityOAuthProvider) Instantiate(ctx *interaction.Context, gra } type NodeUseIdentityOAuthProvider struct { - IsAuthentication bool `json:"is_authentication"` - IsCreating bool `json:"is_creating"` - Config config.OAuthSSOProviderConfig `json:"provider_config"` - HashedNonce string `json:"hashed_nonce"` - ErrorRedirectURI string `json:"error_redirect_uri"` - RedirectURI string `json:"redirect_uri"` + IsAuthentication bool `json:"is_authentication"` + IsCreating bool `json:"is_creating"` + Config oauthrelyingparty.ProviderConfig `json:"provider_config"` + HashedNonce string `json:"hashed_nonce"` + ErrorRedirectURI string `json:"error_redirect_uri"` + RedirectURI string `json:"redirect_uri"` } // GetRedirectURI implements RedirectURIGetter. diff --git a/pkg/lib/interaction/nodes/use_identity_oauth_user_info.go b/pkg/lib/interaction/nodes/use_identity_oauth_user_info.go index 208ce3ef9d..db7b4c8060 100644 --- a/pkg/lib/interaction/nodes/use_identity_oauth_user_info.go +++ b/pkg/lib/interaction/nodes/use_identity_oauth_user_info.go @@ -4,12 +4,12 @@ import ( "crypto/subtle" "fmt" - "github.com/authgear/authgear-server/pkg/api" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/authn/identity" - "github.com/authgear/authgear-server/pkg/lib/authn/sso" - "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/interaction" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil" "github.com/authgear/authgear-server/pkg/util/crypto" ) @@ -28,7 +28,7 @@ type InputUseIdentityOAuthUserInfo interface { type EdgeUseIdentityOAuthUserInfo struct { IsAuthentication bool IsCreating bool - Config config.OAuthSSOProviderConfig + Config oauthrelyingparty.ProviderConfig HashedNonce string ErrorRedirectURI string } @@ -47,23 +47,20 @@ func (e *EdgeUseIdentityOAuthUserInfo) Instantiate(ctx *interaction.Context, gra errorURI := input.GetErrorURI() hashedNonce := e.HashedNonce - if e.Config.Alias != alias { - return nil, fmt.Errorf("interaction: unexpected provider alias %s != %s", e.Config.Alias, alias) - } - - oauthProvider := ctx.OAuthProviderFactory.NewOAuthProvider(alias) - if oauthProvider == nil { - return nil, api.ErrOAuthProviderNotFound + providerConfigAlias := e.Config.Alias() + if providerConfigAlias != alias { + return nil, fmt.Errorf("interaction: unexpected provider alias %s != %s", providerConfigAlias, alias) } // Handle provider error if oauthError != "" { - return nil, sso.NewOAuthError(oauthError, errorDescription, errorURI) + return nil, oauthrelyingpartyutil.NewOAuthError(oauthError, errorDescription, errorURI) } if nonceSource == "" { return nil, fmt.Errorf("nonce does not present in the request") } + nonce := crypto.SHA256String(nonceSource) if subtle.ConstantTimeCompare([]byte(hashedNonce), []byte(nonce)) != 1 { return nil, fmt.Errorf("invalid nonce") @@ -71,11 +68,15 @@ func (e *EdgeUseIdentityOAuthUserInfo) Instantiate(ctx *interaction.Context, gra redirectURI := ctx.OAuthRedirectURIBuilder.SSOCallbackURL(alias) - userInfo, err := oauthProvider.GetAuthInfo( - sso.OAuthAuthorizationResponse{ - Code: code, - }, - sso.GetAuthInfoParam{ + providerConfig, err := ctx.OAuthProviderFactory.GetProviderConfig(alias) + if err != nil { + return nil, err + } + + userInfo, err := ctx.OAuthProviderFactory.GetUserProfile( + alias, + oauthrelyingparty.GetUserProfileOptions{ + Code: code, RedirectURI: redirectURI.String(), Nonce: hashedNonce, }, @@ -84,7 +85,6 @@ func (e *EdgeUseIdentityOAuthUserInfo) Instantiate(ctx *interaction.Context, gra return nil, err } - providerConfig := oauthProvider.Config() providerID := providerConfig.ProviderID() spec := &identity.Spec{ Type: model.IdentityTypeOAuth, @@ -92,7 +92,7 @@ func (e *EdgeUseIdentityOAuthUserInfo) Instantiate(ctx *interaction.Context, gra ProviderID: providerID, SubjectID: userInfo.ProviderUserID, RawProfile: userInfo.ProviderRawProfile, - StandardClaims: userInfo.StandardAttributes.ToClaims(), + StandardClaims: userInfo.StandardAttributes, }, } diff --git a/pkg/lib/oauth/handler/handler_proxy_redirect_test.go b/pkg/lib/oauth/handler/handler_proxy_redirect_test.go index 8696070d2f..7cc84c5ff0 100644 --- a/pkg/lib/oauth/handler/handler_proxy_redirect_test.go +++ b/pkg/lib/oauth/handler/handler_proxy_redirect_test.go @@ -3,9 +3,10 @@ package handler_test import ( "testing" + . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/oauth/handler" - . "github.com/smartystreets/goconvey/convey" ) func TestProxyRedirectHandler(t *testing.T) { diff --git a/pkg/lib/oauthrelyingparty/adfs/provider.go b/pkg/lib/oauthrelyingparty/adfs/provider.go new file mode 100644 index 0000000000..0526a6a341 --- /dev/null +++ b/pkg/lib/oauthrelyingparty/adfs/provider.go @@ -0,0 +1,190 @@ +package adfs + +import ( + "context" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + + "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" + liboauthrelyingparty "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil" + "github.com/authgear/authgear-server/pkg/util/validation" +) + +func init() { + oauthrelyingparty.RegisterProvider(Type, ADFS{}) +} + +const Type = liboauthrelyingparty.TypeADFS + +type ProviderConfig oauthrelyingparty.ProviderConfig + +func (c ProviderConfig) DiscoveryDocumentEndpoint() string { + discovery_document_endpoint, _ := c["discovery_document_endpoint"].(string) + return discovery_document_endpoint +} + +var _ oauthrelyingparty.Provider = ADFS{} +var _ liboauthrelyingparty.BuiltinProvider = ADFS{} + +var Schema = validation.NewSimpleSchema(` +{ + "type": "object", + "additionalProperties": false, + "properties": { + "alias": { "type": "string" }, + "type": { "type": "string" }, + "modify_disabled": { "type": "boolean" }, + "client_id": { "type": "string", "minLength": 1 }, + "claims": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "object", + "additionalProperties": false, + "properties": { + "assume_verified": { "type": "boolean" }, + "required": { "type": "boolean" } + } + } + } + }, + "discovery_document_endpoint": { "type": "string", "format": "uri" } + }, + "required": ["alias", "type", "client_id", "discovery_document_endpoint"] +} +`) + +type ADFS struct{} + +func (ADFS) ValidateProviderConfig(ctx *validation.Context, cfg oauthrelyingparty.ProviderConfig) { + ctx.AddError(Schema.Validator().ValidateValue(cfg)) +} + +func (ADFS) SetDefaults(cfg oauthrelyingparty.ProviderConfig) { + cfg.SetDefaultsModifyDisabledFalse() + cfg.SetDefaultsEmailClaimConfig(oauthrelyingpartyutil.Email_AssumeVerified_Required()) +} + +func (ADFS) ProviderID(cfg oauthrelyingparty.ProviderConfig) oauthrelyingparty.ProviderID { + // In the original implementation, provider ID is just type. + return oauthrelyingparty.NewProviderID(cfg.Type(), nil) +} + +func (ADFS) scope() []string { + // The supported scopes are observed from a AD FS server. + return []string{"openid", "profile", "email"} +} + +func (ADFS) getOpenIDConfiguration(deps oauthrelyingparty.Dependencies) (*oauthrelyingpartyutil.OIDCDiscoveryDocument, error) { + endpoint := ProviderConfig(deps.ProviderConfig).DiscoveryDocumentEndpoint() + return oauthrelyingpartyutil.FetchOIDCDiscoveryDocument(deps.HTTPClient, endpoint) +} + +func (p ADFS) GetAuthorizationURL(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetAuthorizationURLOptions) (string, error) { + c, err := p.getOpenIDConfiguration(deps) + if err != nil { + return "", err + } + return c.MakeOAuthURL(oauthrelyingpartyutil.AuthorizationURLParams{ + ClientID: deps.ProviderConfig.ClientID(), + RedirectURI: param.RedirectURI, + Scope: p.scope(), + ResponseType: oauthrelyingparty.ResponseTypeCode, + ResponseMode: param.ResponseMode, + State: param.State, + Prompt: p.getPrompt(param.Prompt), + Nonce: param.Nonce, + }), nil +} + +func (p ADFS) GetUserProfile(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetUserProfileOptions) (authInfo oauthrelyingparty.UserProfile, err error) { + c, err := p.getOpenIDConfiguration(deps) + if err != nil { + return + } + + // OPTIMIZE(sso): Cache JWKs + keySet, err := c.FetchJWKs(deps.HTTPClient) + if err != nil { + return + } + + var tokenResp oauthrelyingpartyutil.AccessTokenResp + jwtToken, err := c.ExchangeCode( + deps.HTTPClient, + deps.Clock, + param.Code, + keySet, + deps.ProviderConfig.ClientID(), + deps.ClientSecret, + param.RedirectURI, + param.Nonce, + &tokenResp, + ) + if err != nil { + return + } + + claims, err := jwtToken.AsMap(context.TODO()) + if err != nil { + return + } + + sub, ok := claims["sub"].(string) + if !ok { + err = oauthrelyingpartyutil.OAuthProtocolError.New("sub not found in ID token") + return + } + + // The upn claim is documented here. + // https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/configuring-alternate-login-id + upn, ok := claims["upn"].(string) + if !ok { + err = oauthrelyingpartyutil.OAuthProtocolError.New("upn not found in ID token") + return + } + + extracted, err := stdattrs.Extract(claims, stdattrs.ExtractOptions{}) + if err != nil { + return + } + + // Transform upn into preferred_username + if _, ok := extracted[stdattrs.PreferredUsername]; !ok { + extracted[stdattrs.PreferredUsername] = upn + } + // Transform upn into email + if _, ok := extracted[stdattrs.Email]; !ok { + if emailErr := (validation.FormatEmail{}).CheckFormat(upn); emailErr == nil { + // upn looks like an email address. + extracted[stdattrs.Email] = upn + } + } + + emailRequired := deps.ProviderConfig.EmailClaimConfig().Required() + extracted, err = stdattrs.Extract(extracted, stdattrs.ExtractOptions{ + EmailRequired: emailRequired, + }) + if err != nil { + return + } + authInfo.StandardAttributes = extracted + + authInfo.ProviderRawProfile = claims + authInfo.ProviderUserID = sub + + return +} + +func (ADFS) getPrompt(prompt []string) []string { + // ADFS only supports prompt=login + // https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/ad-fs-prompt-login + for _, p := range prompt { + if p == "login" { + return []string{"login"} + } + } + return []string{} +} diff --git a/pkg/lib/authn/sso/adfs_test.go b/pkg/lib/oauthrelyingparty/adfs/provider_test.go similarity index 54% rename from pkg/lib/authn/sso/adfs_test.go rename to pkg/lib/oauthrelyingparty/adfs/provider_test.go index 9a2319843e..659a13e375 100644 --- a/pkg/lib/authn/sso/adfs_test.go +++ b/pkg/lib/oauthrelyingparty/adfs/provider_test.go @@ -1,29 +1,32 @@ -package sso +package adfs import ( "net/http" "testing" - "github.com/authgear/authgear-server/pkg/lib/config" . "github.com/smartystreets/goconvey/convey" "gopkg.in/h2non/gock.v1" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" ) -func TestADFSImpl(t *testing.T) { - Convey("ADFSImpl", t, func() { - client := OAuthHTTPClient{&http.Client{}} - gock.InterceptClient(client.Client) +func TestADFS(t *testing.T) { + Convey("ADFS", t, func() { + client := &http.Client{} + gock.InterceptClient(client) defer gock.Off() - g := &ADFSImpl{ - ProviderConfig: config.OAuthSSOProviderConfig{ - ClientID: "client_id", - Type: config.OAuthSSOProviderTypeADFS, - DiscoveryDocumentEndpoint: "https://localhost/.well-known/openid-configuration", + deps := oauthrelyingparty.Dependencies{ + ProviderConfig: oauthrelyingparty.ProviderConfig{ + "client_id": "client_id", + "type": Type, + "discovery_document_endpoint": "https://localhost/.well-known/openid-configuration", }, HTTPClient: client, } + g := ADFS{} + gock.New("https://localhost/.well-known/openid-configuration"). Reply(200). BodyString(` @@ -33,9 +36,9 @@ func TestADFSImpl(t *testing.T) { `) defer func() { gock.Flush() }() - u, err := g.GetAuthURL(GetAuthURLParam{ + u, err := g.GetAuthorizationURL(deps, oauthrelyingparty.GetAuthorizationURLOptions{ RedirectURI: "https://localhost/", - ResponseMode: ResponseModeFormPost, + ResponseMode: oauthrelyingparty.ResponseModeFormPost, Nonce: "nonce", State: "state", Prompt: []string{"login"}, diff --git a/pkg/lib/oauthrelyingparty/apple/provider.go b/pkg/lib/oauthrelyingparty/apple/provider.go new file mode 100644 index 0000000000..9adeec1051 --- /dev/null +++ b/pkg/lib/oauthrelyingparty/apple/provider.go @@ -0,0 +1,233 @@ +package apple + +import ( + "context" + "strings" + + "github.com/lestrrat-go/jwx/v2/jwa" + "github.com/lestrrat-go/jwx/v2/jwk" + "github.com/lestrrat-go/jwx/v2/jwt" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + + "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" + liboauthrelyingparty "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil" + "github.com/authgear/authgear-server/pkg/util/crypto" + "github.com/authgear/authgear-server/pkg/util/duration" + "github.com/authgear/authgear-server/pkg/util/jwtutil" + "github.com/authgear/authgear-server/pkg/util/validation" +) + +func init() { + oauthrelyingparty.RegisterProvider(Type, Apple{}) +} + +const Type = liboauthrelyingparty.TypeApple + +type ProviderConfig oauthrelyingparty.ProviderConfig + +func (c ProviderConfig) TeamID() string { + team_id, _ := c["team_id"].(string) + return team_id +} + +func (c ProviderConfig) KeyID() string { + key_id, _ := c["key_id"].(string) + return key_id +} + +var _ oauthrelyingparty.Provider = Apple{} +var _ liboauthrelyingparty.BuiltinProvider = Apple{} + +var Schema = validation.NewSimpleSchema(` +{ + "type": "object", + "additionalProperties": false, + "properties": { + "alias": { "type": "string" }, + "type": { "type": "string" }, + "modify_disabled": { "type": "boolean" }, + "client_id": { "type": "string", "minLength": 1 }, + "claims": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "object", + "additionalProperties": false, + "properties": { + "assume_verified": { "type": "boolean" }, + "required": { "type": "boolean" } + } + } + } + }, + "key_id": { "type": "string" }, + "team_id": { "type": "string" } + }, + "required": ["alias", "type", "client_id", "key_id", "team_id"] +} +`) + +var appleOIDCConfig = oauthrelyingpartyutil.OIDCDiscoveryDocument{ + JWKSUri: "https://appleid.apple.com/auth/keys", + TokenEndpoint: "https://appleid.apple.com/auth/token", + AuthorizationEndpoint: "https://appleid.apple.com/auth/authorize", +} + +type Apple struct{} + +func (Apple) ValidateProviderConfig(ctx *validation.Context, cfg oauthrelyingparty.ProviderConfig) { + ctx.AddError(Schema.Validator().ValidateValue(cfg)) +} + +func (Apple) SetDefaults(cfg oauthrelyingparty.ProviderConfig) { + cfg.SetDefaultsModifyDisabledFalse() + cfg.SetDefaultsEmailClaimConfig(oauthrelyingpartyutil.Email_AssumeVerified_Required()) +} + +func (Apple) ProviderID(cfg oauthrelyingparty.ProviderConfig) oauthrelyingparty.ProviderID { + team_id := ProviderConfig(cfg).TeamID() + // Apple supports OIDC. + // sub is pairwise and is scoped to team_id. + // Therefore, ProviderID is Type + team_id. + // + // Rotating the OAuth application is OK. + // But rotating the Apple Developer account is problematic. + // Since Apple has private relay to hide the real email, + // the user may not be associate their account. + keys := map[string]interface{}{ + "team_id": team_id, + } + return oauthrelyingparty.NewProviderID(cfg.Type(), keys) +} + +func (Apple) scope() []string { + // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms + return []string{"name", "email"} +} + +func (Apple) createClientSecret(deps oauthrelyingparty.Dependencies) (clientSecret string, err error) { + teamID := ProviderConfig(deps.ProviderConfig).TeamID() + keyID := ProviderConfig(deps.ProviderConfig).KeyID() + + // https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens + key, err := crypto.ParseAppleP8PrivateKey([]byte(deps.ClientSecret)) + if err != nil { + return + } + + now := deps.Clock.NowUTC() + + payload := jwt.New() + _ = payload.Set(jwt.IssuerKey, teamID) + _ = payload.Set(jwt.IssuedAtKey, now.Unix()) + _ = payload.Set(jwt.ExpirationKey, now.Add(duration.Short).Unix()) + _ = payload.Set(jwt.AudienceKey, "https://appleid.apple.com") + _ = payload.Set(jwt.SubjectKey, deps.ProviderConfig.ClientID) + + jwkKey, err := jwk.FromRaw(key) + if err != nil { + return + } + _ = jwkKey.Set("kid", keyID) + + token, err := jwtutil.Sign(payload, jwa.ES256, jwkKey) + if err != nil { + return + } + + clientSecret = string(token) + return +} + +func (p Apple) GetAuthorizationURL(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetAuthorizationURLOptions) (string, error) { + return appleOIDCConfig.MakeOAuthURL(oauthrelyingpartyutil.AuthorizationURLParams{ + ClientID: deps.ProviderConfig.ClientID(), + RedirectURI: param.RedirectURI, + Scope: p.scope(), + ResponseType: oauthrelyingparty.ResponseTypeCode, + ResponseMode: param.ResponseMode, + State: param.State, + // Prompt is unset. + // Apple doesn't support prompt parameter + // See "Send the Required Query Parameters" section for supporting parameters + // https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/incorporating_sign_in_with_apple_into_other_platforms + Nonce: param.Nonce, + }), nil +} + +func (p Apple) GetUserProfile(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetUserProfileOptions) (authInfo oauthrelyingparty.UserProfile, err error) { + keySet, err := appleOIDCConfig.FetchJWKs(deps.HTTPClient) + if err != nil { + return + } + + clientSecret, err := p.createClientSecret(deps) + if err != nil { + return + } + + var tokenResp oauthrelyingpartyutil.AccessTokenResp + jwtToken, err := appleOIDCConfig.ExchangeCode( + deps.HTTPClient, + deps.Clock, + param.Code, + keySet, + deps.ProviderConfig.ClientID(), + clientSecret, + param.RedirectURI, + param.Nonce, + &tokenResp, + ) + if err != nil { + return + } + + claims, err := jwtToken.AsMap(context.TODO()) + if err != nil { + return + } + + // Verify the issuer + // https://developer.apple.com/documentation/signinwithapplerestapi/verifying_a_user + // The exact spec is + // Verify that the iss field contains https://appleid.apple.com + // Therefore, we use strings.Contains here. + iss, ok := claims["iss"].(string) + if !ok { + err = oauthrelyingpartyutil.OAuthProtocolError.New("iss not found in ID token") + return + } + if !strings.Contains(iss, "https://appleid.apple.com") { + err = oauthrelyingpartyutil.OAuthProtocolError.New("iss does not equal to `https://appleid.apple.com`") + return + } + + // Ensure sub exists + sub, ok := claims["sub"].(string) + if !ok { + err = oauthrelyingpartyutil.OAuthProtocolError.New("sub not found in ID Token") + return + } + + // By observation, if the first time of authentication does NOT include the `name` scope, + // Even the Services ID is unauthorized on https://appleid.apple.com, + // and the `name` scope is included, + // The ID Token still does not include the `name` claim. + + authInfo.ProviderRawProfile = claims + authInfo.ProviderUserID = sub + + emailRequired := deps.ProviderConfig.EmailClaimConfig().Required() + stdAttrs, err := stdattrs.Extract(claims, stdattrs.ExtractOptions{ + EmailRequired: emailRequired, + }) + if err != nil { + return + } + authInfo.StandardAttributes = stdAttrs.WithNameCopiedToGivenName() + + return +} diff --git a/pkg/lib/authn/sso/apple_test.go b/pkg/lib/oauthrelyingparty/apple/provider_test.go similarity index 57% rename from pkg/lib/authn/sso/apple_test.go rename to pkg/lib/oauthrelyingparty/apple/provider_test.go index 49a9328176..7af04ac170 100644 --- a/pkg/lib/authn/sso/apple_test.go +++ b/pkg/lib/oauthrelyingparty/apple/provider_test.go @@ -1,25 +1,26 @@ -package sso +package apple import ( "testing" - "github.com/authgear/authgear-server/pkg/lib/config" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" ) func TestAppleImpl(t *testing.T) { Convey("AppleImpl", t, func() { - g := &AppleImpl{ - ProviderConfig: config.OAuthSSOProviderConfig{ - ClientID: "client_id", - Type: config.OAuthSSOProviderTypeApple, + deps := oauthrelyingparty.Dependencies{ + ProviderConfig: oauthrelyingparty.ProviderConfig{ + "client_id": "client_id", + "type": Type, }, - HTTPClient: OAuthHTTPClient{}, } + g := Apple{} - u, err := g.GetAuthURL(GetAuthURLParam{ + u, err := g.GetAuthorizationURL(deps, oauthrelyingparty.GetAuthorizationURLOptions{ RedirectURI: "https://localhost/", - ResponseMode: ResponseModeFormPost, + ResponseMode: oauthrelyingparty.ResponseModeFormPost, Nonce: "nonce", State: "state", Prompt: []string{"login"}, diff --git a/pkg/lib/oauthrelyingparty/azureadb2c/provider.go b/pkg/lib/oauthrelyingparty/azureadb2c/provider.go new file mode 100644 index 0000000000..9d3471761e --- /dev/null +++ b/pkg/lib/oauthrelyingparty/azureadb2c/provider.go @@ -0,0 +1,259 @@ +package azureadb2c + +import ( + "context" + "fmt" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + + "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" + liboauthrelyingparty "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil" + "github.com/authgear/authgear-server/pkg/util/validation" +) + +func init() { + oauthrelyingparty.RegisterProvider(Type, AzureADB2C{}) +} + +const Type = liboauthrelyingparty.TypeAzureADB2C + +type ProviderConfig oauthrelyingparty.ProviderConfig + +func (c ProviderConfig) Tenant() string { + tenant, _ := c["tenant"].(string) + return tenant +} + +func (c ProviderConfig) Policy() string { + policy, _ := c["policy"].(string) + return policy +} + +var _ oauthrelyingparty.Provider = AzureADB2C{} +var _ liboauthrelyingparty.BuiltinProvider = AzureADB2C{} + +var Schema = validation.NewSimpleSchema(` +{ + "type": "object", + "additionalProperties": false, + "properties": { + "alias": { "type": "string" }, + "type": { "type": "string" }, + "modify_disabled": { "type": "boolean" }, + "client_id": { "type": "string", "minLength": 1 }, + "claims": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "object", + "additionalProperties": false, + "properties": { + "assume_verified": { "type": "boolean" }, + "required": { "type": "boolean" } + } + } + } + }, + "tenant": { "type": "string" }, + "policy": { "type": "string" } + }, + "required": ["alias", "type", "client_id", "tenant", "policy"] +} +`) + +type AzureADB2C struct{} + +func (AzureADB2C) ValidateProviderConfig(ctx *validation.Context, cfg oauthrelyingparty.ProviderConfig) { + ctx.AddError(Schema.Validator().ValidateValue(cfg)) +} + +func (AzureADB2C) SetDefaults(cfg oauthrelyingparty.ProviderConfig) { + cfg.SetDefaultsModifyDisabledFalse() + cfg.SetDefaultsEmailClaimConfig(oauthrelyingpartyutil.Email_AssumeVerified_Required()) +} + +func (AzureADB2C) ProviderID(cfg oauthrelyingparty.ProviderConfig) oauthrelyingparty.ProviderID { + // By default sub is the Object ID of the user in the directory. + // A tenant is a directory. + // sub is scoped to the tenant only. + // Therefore, ProviderID is Type + tenant. + // + // See https://docs.microsoft.com/en-us/azure/active-directory-b2c/tokens-overview#claims + tenant := ProviderConfig(cfg).Tenant() + keys := map[string]interface{}{ + "tenant": tenant, + } + return oauthrelyingparty.NewProviderID(cfg.Type(), keys) +} + +func (AzureADB2C) scope() []string { + // Instead of specifying scope to request a specific claim, + // the developer must customize the policy to allow which claims are returned to the relying party. + // If the developer is using User Flow policy, then those claims are called Application Claims. + return []string{"openid"} +} + +func (AzureADB2C) getOpenIDConfiguration(deps oauthrelyingparty.Dependencies) (*oauthrelyingpartyutil.OIDCDiscoveryDocument, error) { + azureadb2cConfig := ProviderConfig(deps.ProviderConfig) + tenant := azureadb2cConfig.Tenant() + policy := azureadb2cConfig.Policy() + + endpoint := fmt.Sprintf( + "https://%s.b2clogin.com/%s.onmicrosoft.com/%s/v2.0/.well-known/openid-configuration", + tenant, + tenant, + policy, + ) + + return oauthrelyingpartyutil.FetchOIDCDiscoveryDocument(deps.HTTPClient, endpoint) +} + +func (p AzureADB2C) GetAuthorizationURL(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetAuthorizationURLOptions) (string, error) { + c, err := p.getOpenIDConfiguration(deps) + if err != nil { + return "", err + } + return c.MakeOAuthURL(oauthrelyingpartyutil.AuthorizationURLParams{ + ClientID: deps.ProviderConfig.ClientID(), + RedirectURI: param.RedirectURI, + Scope: p.scope(), + ResponseType: oauthrelyingparty.ResponseTypeCode, + ResponseMode: param.ResponseMode, + State: param.State, + Prompt: p.getPrompt(param.Prompt), + Nonce: param.Nonce, + }), nil +} + +func (p AzureADB2C) GetUserProfile(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetUserProfileOptions) (authInfo oauthrelyingparty.UserProfile, err error) { + c, err := p.getOpenIDConfiguration(deps) + if err != nil { + return + } + // OPTIMIZE(sso): Cache JWKs + keySet, err := c.FetchJWKs(deps.HTTPClient) + if err != nil { + return + } + + var tokenResp oauthrelyingpartyutil.AccessTokenResp + jwtToken, err := c.ExchangeCode( + deps.HTTPClient, + deps.Clock, + param.Code, + keySet, + deps.ProviderConfig.ClientID(), + deps.ClientSecret, + param.RedirectURI, + param.Nonce, + &tokenResp, + ) + if err != nil { + return + } + + claims, err := jwtToken.AsMap(context.TODO()) + if err != nil { + return + } + + iss, ok := claims["iss"].(string) + if !ok { + err = oauthrelyingpartyutil.OAuthProtocolError.New("iss not found in ID token") + return + } + if iss != c.Issuer { + err = oauthrelyingpartyutil.OAuthProtocolError.New( + fmt.Sprintf("iss: %v != %v", iss, c.Issuer), + ) + return + } + + sub, ok := claims["sub"].(string) + if !ok || sub == "" { + err = oauthrelyingpartyutil.OAuthProtocolError.New("sub not found in ID Token") + return + } + + authInfo.ProviderRawProfile = claims + authInfo.ProviderUserID = sub + + stdAttrs, err := p.extract(deps, claims) + if err != nil { + return + } + authInfo.StandardAttributes = stdAttrs + + return +} + +func (AzureADB2C) extract(deps oauthrelyingparty.Dependencies, claims map[string]interface{}) (stdattrs.T, error) { + // Here is the list of possible builtin claims of user flows + // https://learn.microsoft.com/en-us/azure/active-directory-b2c/user-flow-overview#user-flows + // city: free text + // country: free text + // jobTitle: free text + // legalAgeGroupClassification: a enum with undocumented variants + // postalCode: free text + // state: free text + // streetAddress: free text + // newUser: true means the user signed up newly + // oid: sub is identical to it by default. + // emails: if non-empty, the first value corresponds to standard claim + // name: correspond to standard claim + // given_name: correspond to standard claim + // family_name: correspond to standard claim + + // For custom policy we further recognize the following claims. + // https://learn.microsoft.com/en-us/azure/active-directory-b2c/user-profile-attributes + // signInNames.emailAddress: string + + extractString := func(input map[string]interface{}, output stdattrs.T, key string) { + if value, ok := input[key].(string); ok && value != "" { + output[key] = value + } + } + + out := stdattrs.T{} + + extractString(claims, out, stdattrs.Name) + extractString(claims, out, stdattrs.GivenName) + extractString(claims, out, stdattrs.FamilyName) + + var email string + if email == "" { + if ifaceSlice, ok := claims["emails"].([]interface{}); ok { + for _, iface := range ifaceSlice { + if str, ok := iface.(string); ok && str != "" { + email = str + } + } + } + } + if email == "" { + if str, ok := claims["signInNames.emailAddress"].(string); ok { + if str != "" { + email = str + } + } + } + out[stdattrs.Email] = email + + emailRequired := deps.ProviderConfig.EmailClaimConfig().Required() + return stdattrs.Extract(out, stdattrs.ExtractOptions{ + EmailRequired: emailRequired, + }) +} + +func (AzureADB2C) getPrompt(prompt []string) []string { + // The only supported value is login. + // See https://docs.microsoft.com/en-us/azure/active-directory-b2c/openid-connect + for _, p := range prompt { + if p == "login" { + return []string{"login"} + } + } + return []string{} +} diff --git a/pkg/lib/authn/sso/azureadb2c_test.go b/pkg/lib/oauthrelyingparty/azureadb2c/provider_test.go similarity index 62% rename from pkg/lib/authn/sso/azureadb2c_test.go rename to pkg/lib/oauthrelyingparty/azureadb2c/provider_test.go index 2b0c741cf9..89009d77ea 100644 --- a/pkg/lib/authn/sso/azureadb2c_test.go +++ b/pkg/lib/oauthrelyingparty/azureadb2c/provider_test.go @@ -1,30 +1,33 @@ -package sso +package azureadb2c import ( "net/http" "testing" - "github.com/authgear/authgear-server/pkg/lib/config" . "github.com/smartystreets/goconvey/convey" "gopkg.in/h2non/gock.v1" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" ) func TestAzureadb2cImpl(t *testing.T) { Convey("Azureadb2cImpl", t, func() { - client := OAuthHTTPClient{&http.Client{}} - gock.InterceptClient(client.Client) + client := &http.Client{} + gock.InterceptClient(client) defer gock.Off() - g := &Azureadb2cImpl{ - ProviderConfig: config.OAuthSSOProviderConfig{ - ClientID: "client_id", - Type: config.OAuthSSOProviderTypeAzureADB2C, - Tenant: "tenant", - Policy: "policy", + deps := oauthrelyingparty.Dependencies{ + ProviderConfig: oauthrelyingparty.ProviderConfig{ + "client_id": "client_id", + "type": Type, + "tenant": "tenant", + "policy": "policy", }, HTTPClient: client, } + g := AzureADB2C{} + gock.New("https://tenant.b2clogin.com/tenant.onmicrosoft.com/policy/v2.0/.well-known/openid-configuration"). Reply(200). BodyString(` @@ -34,9 +37,9 @@ func TestAzureadb2cImpl(t *testing.T) { `) defer func() { gock.Flush() }() - u, err := g.GetAuthURL(GetAuthURLParam{ + u, err := g.GetAuthorizationURL(deps, oauthrelyingparty.GetAuthorizationURLOptions{ RedirectURI: "https://localhost/", - ResponseMode: ResponseModeFormPost, + ResponseMode: oauthrelyingparty.ResponseModeFormPost, Nonce: "nonce", State: "state", Prompt: []string{"login"}, diff --git a/pkg/lib/oauthrelyingparty/azureadv2/provider.go b/pkg/lib/oauthrelyingparty/azureadv2/provider.go new file mode 100644 index 0000000000..3b3ddad909 --- /dev/null +++ b/pkg/lib/oauthrelyingparty/azureadv2/provider.go @@ -0,0 +1,229 @@ +package azureadv2 + +import ( + "context" + "fmt" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + + "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" + liboauthrelyingparty "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil" + "github.com/authgear/authgear-server/pkg/util/validation" +) + +func init() { + oauthrelyingparty.RegisterProvider(Type, AzureADv2{}) +} + +const Type = liboauthrelyingparty.TypeAzureADv2 + +type ProviderConfig oauthrelyingparty.ProviderConfig + +func (c ProviderConfig) Tenant() string { + tenant, _ := c["tenant"].(string) + return tenant +} + +var _ oauthrelyingparty.Provider = AzureADv2{} +var _ liboauthrelyingparty.BuiltinProvider = AzureADv2{} + +var Schema = validation.NewSimpleSchema(` +{ + "type": "object", + "additionalProperties": false, + "properties": { + "alias": { "type": "string" }, + "type": { "type": "string" }, + "modify_disabled": { "type": "boolean" }, + "client_id": { "type": "string", "minLength": 1 }, + "claims": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "object", + "additionalProperties": false, + "properties": { + "assume_verified": { "type": "boolean" }, + "required": { "type": "boolean" } + } + } + } + }, + "tenant": { "type": "string" } + }, + "required": ["alias", "type", "client_id", "tenant"] +} +`) + +type AzureADv2 struct{} + +func (AzureADv2) ValidateProviderConfig(ctx *validation.Context, cfg oauthrelyingparty.ProviderConfig) { + ctx.AddError(Schema.Validator().ValidateValue(cfg)) +} + +func (AzureADv2) SetDefaults(cfg oauthrelyingparty.ProviderConfig) { + cfg.SetDefaultsModifyDisabledFalse() + cfg.SetDefaultsEmailClaimConfig(oauthrelyingpartyutil.Email_AssumeVerified_Required()) +} + +func (AzureADv2) ProviderID(cfg oauthrelyingparty.ProviderConfig) oauthrelyingparty.ProviderID { + // Azure AD v2 supports OIDC. + // sub is pairwise and is scoped to client_id. + // However, oid is powerful alternative to sub. + // oid is also pairwise and is scoped to tenant. + // We use oid as ProviderSubjectID so ProviderID is Type + tenant. + // + // Rotating the OAuth application is OK. + // But rotating the tenant is problematic. + // But if email remains unchanged, the user can associate their account. + tenant := ProviderConfig(cfg).Tenant() + keys := map[string]interface{}{ + "tenant": tenant, + } + return oauthrelyingparty.NewProviderID(cfg.Type(), keys) +} + +func (AzureADv2) scope() []string { + // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes + return []string{"openid", "profile", "email"} +} + +func (AzureADv2) getOpenIDConfiguration(deps oauthrelyingparty.Dependencies) (*oauthrelyingpartyutil.OIDCDiscoveryDocument, error) { + // OPTIMIZE(sso): Cache OpenID configuration + + tenant := ProviderConfig(deps.ProviderConfig).Tenant() + + var endpoint string + // Azure special tenant + // + // If the azure tenant is `organizations` or `common`, + // the developer should make use of `before_user_create` and `before_identity_create` hook + // to disallow any undesire identity. + // The `raw_profile` of the identity is the ID Token claims. + // Refer to https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens + // to see what claims the token could contain. + // + // For `organizations`, the user can be any user of any organizational AD. + // Therefore the developer should have a whitelist of AD tenant IDs. + // In the incoming hook, check if `tid` matches one of the entry of the whitelist. + // + // For `common`, in addition to the users from `organizations`, any Microsoft personal account + // could be the user. + // In case of personal account, the `tid` is "9188040d-6c67-4c5b-b112-36a304b66dad". + // Therefore the developer should first check if `tid` indicates personal account. + // If yes, apply their logic to disallow the user creation. + // One very common example is to look at the claim `email`. + // Use a email address parser to parse the email address. + // Obtain the domain and check if the domain is whitelisted. + // For example, if the developer only wants user from hotmail.com to create user, + // ensure `tid` is "9188040d-6c67-4c5b-b112-36a304b66dad" and ensure `email` + // is of domain `@hotmail.com`. + + // As of 2019-09-23, two special values are observed. + // To discover these values, create a new client + // and try different options. + switch tenant { + // Special value for any organizational AD + case "organizations": + endpoint = "https://login.microsoftonline.com/organizations/v2.0/.well-known/openid-configuration" + // Special value for any organizational AD and personal accounts (Xbox etc) + case "common": + endpoint = "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration" + default: + endpoint = fmt.Sprintf("https://login.microsoftonline.com/%s/v2.0/.well-known/openid-configuration", tenant) + } + + return oauthrelyingpartyutil.FetchOIDCDiscoveryDocument(deps.HTTPClient, endpoint) +} + +func (p AzureADv2) GetAuthorizationURL(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetAuthorizationURLOptions) (string, error) { + c, err := p.getOpenIDConfiguration(deps) + if err != nil { + return "", err + } + return c.MakeOAuthURL(oauthrelyingpartyutil.AuthorizationURLParams{ + ClientID: deps.ProviderConfig.ClientID(), + RedirectURI: param.RedirectURI, + Scope: p.scope(), + ResponseType: oauthrelyingparty.ResponseTypeCode, + ResponseMode: param.ResponseMode, + State: param.State, + Prompt: p.getPrompt(param.Prompt), + Nonce: param.Nonce, + }), nil +} + +func (p AzureADv2) GetUserProfile(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetUserProfileOptions) (authInfo oauthrelyingparty.UserProfile, err error) { + c, err := p.getOpenIDConfiguration(deps) + if err != nil { + return + } + // OPTIMIZE(sso): Cache JWKs + keySet, err := c.FetchJWKs(deps.HTTPClient) + if err != nil { + return + } + + var tokenResp oauthrelyingpartyutil.AccessTokenResp + jwtToken, err := c.ExchangeCode( + deps.HTTPClient, + deps.Clock, + param.Code, + keySet, + deps.ProviderConfig.ClientID(), + deps.ClientSecret, + param.RedirectURI, + param.Nonce, + &tokenResp, + ) + if err != nil { + return + } + + claims, err := jwtToken.AsMap(context.TODO()) + if err != nil { + return + } + + oid, ok := claims["oid"].(string) + if !ok { + err = oauthrelyingpartyutil.OAuthProtocolError.New("oid not found in ID Token") + return + } + // For "Microsoft Account", email usually exists. + // For "AD guest user", email usually exists because to invite an user, the inviter must provide email. + // For "AD user", email never exists even one is provided in "Authentication Methods". + + authInfo.ProviderRawProfile = claims + authInfo.ProviderUserID = oid + emailRequired := deps.ProviderConfig.EmailClaimConfig().Required() + stdAttrs, err := stdattrs.Extract(claims, stdattrs.ExtractOptions{ + EmailRequired: emailRequired, + }) + if err != nil { + return + } + authInfo.StandardAttributes = stdAttrs + + return +} + +func (AzureADv2) getPrompt(prompt []string) []string { + // Azureadv2 only supports single value for prompt. + // The first supporting value in the list will be used. + // The usage of `none` is for checking existing authentication and/or consent + // which doesn't fit auth ui case. + // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow + for _, p := range prompt { + if p == "login" { + return []string{"login"} + } else if p == "consent" { + return []string{"consent"} + } else if p == "select_account" { + return []string{"select_account"} + } + } + return []string{} +} diff --git a/pkg/lib/authn/sso/azureadv2_test.go b/pkg/lib/oauthrelyingparty/azureadv2/provider_test.go similarity index 84% rename from pkg/lib/authn/sso/azureadv2_test.go rename to pkg/lib/oauthrelyingparty/azureadv2/provider_test.go index 265494d699..b1ad2dd297 100644 --- a/pkg/lib/authn/sso/azureadv2_test.go +++ b/pkg/lib/oauthrelyingparty/azureadv2/provider_test.go @@ -1,29 +1,32 @@ -package sso +package azureadv2 import ( "net/http" "testing" - "github.com/authgear/authgear-server/pkg/lib/config" . "github.com/smartystreets/goconvey/convey" "gopkg.in/h2non/gock.v1" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" ) func TestAzureadv2Impl(t *testing.T) { Convey("Azureadv2Impl", t, func() { - client := OAuthHTTPClient{&http.Client{}} - gock.InterceptClient(client.Client) + client := &http.Client{} + gock.InterceptClient(client) defer gock.Off() - g := &Azureadv2Impl{ - ProviderConfig: config.OAuthSSOProviderConfig{ - ClientID: "client_id", - Type: config.OAuthSSOProviderTypeAzureADv2, - Tenant: "common", + deps := oauthrelyingparty.Dependencies{ + ProviderConfig: oauthrelyingparty.ProviderConfig{ + "client_id": "client_id", + "type": Type, + "tenant": "common", }, HTTPClient: client, } + g := AzureADv2{} + gock.New("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"). Reply(200). BodyString(` @@ -97,9 +100,9 @@ func TestAzureadv2Impl(t *testing.T) { `) defer func() { gock.Flush() }() - u, err := g.GetAuthURL(GetAuthURLParam{ + u, err := g.GetAuthorizationURL(deps, oauthrelyingparty.GetAuthorizationURLOptions{ RedirectURI: "https://localhost/", - ResponseMode: ResponseModeFormPost, + ResponseMode: oauthrelyingparty.ResponseModeFormPost, Nonce: "nonce", State: "state", Prompt: []string{"login"}, diff --git a/pkg/lib/oauthrelyingparty/builtin.go b/pkg/lib/oauthrelyingparty/builtin.go new file mode 100644 index 0000000000..a568990fd7 --- /dev/null +++ b/pkg/lib/oauthrelyingparty/builtin.go @@ -0,0 +1,35 @@ +package oauthrelyingparty + +import ( + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + + "github.com/authgear/authgear-server/pkg/util/validation" +) + +const ( + TypeGoogle = "google" + TypeFacebook = "facebook" + TypeGithub = "github" + TypeLinkedin = "linkedin" + TypeAzureADv2 = "azureadv2" + TypeAzureADB2C = "azureadb2c" + TypeADFS = "adfs" + TypeApple = "apple" + TypeWechat = "wechat" +) + +var BuiltinProviderTypes = []string{ + TypeGoogle, + TypeFacebook, + TypeGithub, + TypeLinkedin, + TypeAzureADv2, + TypeAzureADB2C, + TypeADFS, + TypeApple, + TypeWechat, +} + +type BuiltinProvider interface { + ValidateProviderConfig(ctx *validation.Context, providerConfig oauthrelyingparty.ProviderConfig) +} diff --git a/pkg/lib/oauthrelyingparty/facebook/provider.go b/pkg/lib/oauthrelyingparty/facebook/provider.go new file mode 100644 index 0000000000..97af0fbd0f --- /dev/null +++ b/pkg/lib/oauthrelyingparty/facebook/provider.go @@ -0,0 +1,186 @@ +package facebook + +import ( + "net/url" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + + "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" + liboauthrelyingparty "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil" + "github.com/authgear/authgear-server/pkg/util/crypto" + "github.com/authgear/authgear-server/pkg/util/validation" +) + +func init() { + oauthrelyingparty.RegisterProvider(Type, Facebook{}) +} + +const Type = liboauthrelyingparty.TypeFacebook + +var _ oauthrelyingparty.Provider = Facebook{} +var _ liboauthrelyingparty.BuiltinProvider = Facebook{} + +var Schema = validation.NewSimpleSchema(` +{ + "type": "object", + "additionalProperties": false, + "properties": { + "alias": { "type": "string" }, + "type": { "type": "string" }, + "modify_disabled": { "type": "boolean" }, + "client_id": { "type": "string", "minLength": 1 }, + "claims": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "object", + "additionalProperties": false, + "properties": { + "assume_verified": { "type": "boolean" }, + "required": { "type": "boolean" } + } + } + } + } + }, + "required": ["alias", "type", "client_id"] +} +`) + +const ( + facebookAuthorizationURL string = "https://www.facebook.com/v11.0/dialog/oauth" + // nolint: gosec + facebookTokenURL string = "https://graph.facebook.com/v11.0/oauth/access_token" + facebookUserInfoURL string = "https://graph.facebook.com/v11.0/me?fields=id,email,first_name,last_name,middle_name,name,name_format,picture,short_name" +) + +type Facebook struct{} + +func (Facebook) ValidateProviderConfig(ctx *validation.Context, cfg oauthrelyingparty.ProviderConfig) { + ctx.AddError(Schema.Validator().ValidateValue(cfg)) +} + +func (Facebook) SetDefaults(cfg oauthrelyingparty.ProviderConfig) { + cfg.SetDefaultsModifyDisabledFalse() + cfg.SetDefaultsEmailClaimConfig(oauthrelyingpartyutil.Email_AssumeVerified_Required()) +} + +func (Facebook) ProviderID(cfg oauthrelyingparty.ProviderConfig) oauthrelyingparty.ProviderID { + // Facebook does NOT support OIDC. + // Facebook user ID is scoped to client_id. + // Therefore, ProviderID is Type + client_id. + // + // Rotating the OAuth application is problematic. + // But if email remains unchanged, the user can associate their account. + keys := map[string]interface{}{ + "client_id": cfg.ClientID(), + } + return oauthrelyingparty.NewProviderID(cfg.Type(), keys) +} + +func (Facebook) scope() []string { + // https://developers.facebook.com/docs/permissions/reference + return []string{"email", "public_profile"} +} + +func (p Facebook) GetAuthorizationURL(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetAuthorizationURLOptions) (string, error) { + return oauthrelyingpartyutil.MakeAuthorizationURL(facebookAuthorizationURL, oauthrelyingpartyutil.AuthorizationURLParams{ + ClientID: deps.ProviderConfig.ClientID(), + RedirectURI: param.RedirectURI, + Scope: p.scope(), + ResponseType: oauthrelyingparty.ResponseTypeCode, + // ResponseMode is unset + State: param.State, + // Prompt is unset. + // Facebook doesn't support prompt parameter + // https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/ + + // Nonce is unset + }.Query()), nil +} + +func (Facebook) GetUserProfile(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetUserProfileOptions) (authInfo oauthrelyingparty.UserProfile, err error) { + authInfo = oauthrelyingparty.UserProfile{} + + accessTokenResp, err := oauthrelyingpartyutil.FetchAccessTokenResp( + deps.HTTPClient, + param.Code, + facebookTokenURL, + param.RedirectURI, + deps.ProviderConfig.ClientID(), + deps.ClientSecret, + ) + if err != nil { + return + } + + userProfileURL, err := url.Parse(facebookUserInfoURL) + if err != nil { + return + } + q := userProfileURL.Query() + appSecretProof := crypto.HMACSHA256String([]byte(deps.ClientSecret), []byte(accessTokenResp.AccessToken())) + q.Set("appsecret_proof", appSecretProof) + userProfileURL.RawQuery = q.Encode() + + // Here is the refacted user profile of Louis' facebook account. + // { + // "id": "redacted", + // "email": "redacted", + // "first_name": "Jonathan", + // "last_name": "Doe", + // "name": "Johnathan Doe", + // "name_format": "{first} {last}", + // "picture": { + // "data": { + // "height": 50, + // "is_silhouette": true, + // "url": "http://example.com", + // "width": 50 + // } + // }, + // "short_name": "John" + // } + + userProfile, err := oauthrelyingpartyutil.FetchUserProfile(deps.HTTPClient, accessTokenResp, userProfileURL.String()) + if err != nil { + return + } + authInfo.ProviderRawProfile = userProfile + + id, _ := userProfile["id"].(string) + email, _ := userProfile["email"].(string) + firstName, _ := userProfile["first_name"].(string) + lastName, _ := userProfile["last_name"].(string) + name, _ := userProfile["name"].(string) + shortName, _ := userProfile["short_name"].(string) + var picture string + if pictureObj, ok := userProfile["picture"].(map[string]interface{}); ok { + if data, ok := pictureObj["data"].(map[string]interface{}); ok { + if url, ok := data["url"].(string); ok { + picture = url + } + } + } + + authInfo.ProviderUserID = id + emailRequired := deps.ProviderConfig.EmailClaimConfig().Required() + stdAttrs, err := stdattrs.Extract(map[string]interface{}{ + stdattrs.Email: email, + stdattrs.GivenName: firstName, + stdattrs.FamilyName: lastName, + stdattrs.Name: name, + stdattrs.Nickname: shortName, + stdattrs.Picture: picture, + }, stdattrs.ExtractOptions{ + EmailRequired: emailRequired, + }) + if err != nil { + return + } + authInfo.StandardAttributes = stdAttrs + + return +} diff --git a/pkg/lib/authn/sso/facebook_test.go b/pkg/lib/oauthrelyingparty/facebook/provider_test.go similarity index 51% rename from pkg/lib/authn/sso/facebook_test.go rename to pkg/lib/oauthrelyingparty/facebook/provider_test.go index 9c3c48be8a..68cbfe8d64 100644 --- a/pkg/lib/authn/sso/facebook_test.go +++ b/pkg/lib/oauthrelyingparty/facebook/provider_test.go @@ -1,23 +1,24 @@ -package sso +package facebook import ( "testing" - "github.com/authgear/authgear-server/pkg/lib/config" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" ) -func TestFacebookImpl(t *testing.T) { - Convey("FacebookImpl", t, func() { - g := &FacebookImpl{ - ProviderConfig: config.OAuthSSOProviderConfig{ - ClientID: "client_id", - Type: config.OAuthSSOProviderTypeFacebook, +func TestFacebook(t *testing.T) { + Convey("Facebook", t, func() { + deps := oauthrelyingparty.Dependencies{ + ProviderConfig: oauthrelyingparty.ProviderConfig{ + "client_id": "client_id", + "type": Type, }, - HTTPClient: OAuthHTTPClient{}, } + g := Facebook{} - u, err := g.GetAuthURL(GetAuthURLParam{ + u, err := g.GetAuthorizationURL(deps, oauthrelyingparty.GetAuthorizationURLOptions{ RedirectURI: "https://localhost/", Nonce: "nonce", State: "state", diff --git a/pkg/lib/oauthrelyingparty/github/provider.go b/pkg/lib/oauthrelyingparty/github/provider.go new file mode 100644 index 0000000000..722563d68a --- /dev/null +++ b/pkg/lib/oauthrelyingparty/github/provider.go @@ -0,0 +1,211 @@ +package github + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + + "github.com/authgear/authgear-server/pkg/api/apierrors" + "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" + liboauthrelyingparty "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil" + "github.com/authgear/authgear-server/pkg/util/errorutil" + "github.com/authgear/authgear-server/pkg/util/validation" +) + +func init() { + oauthrelyingparty.RegisterProvider(Type, Github{}) +} + +const Type = liboauthrelyingparty.TypeGithub + +var _ oauthrelyingparty.Provider = Github{} +var _ liboauthrelyingparty.BuiltinProvider = Github{} + +var Schema = validation.NewSimpleSchema(` +{ + "type": "object", + "additionalProperties": false, + "properties": { + "alias": { "type": "string" }, + "type": { "type": "string" }, + "modify_disabled": { "type": "boolean" }, + "client_id": { "type": "string", "minLength": 1 }, + "claims": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "object", + "additionalProperties": false, + "properties": { + "assume_verified": { "type": "boolean" }, + "required": { "type": "boolean" } + } + } + } + } + }, + "required": ["alias", "type", "client_id"] +} +`) + +const ( + githubAuthorizationURL string = "https://github.com/login/oauth/authorize" + // nolint: gosec + githubTokenURL string = "https://github.com/login/oauth/access_token" + githubUserInfoURL string = "https://api.github.com/user" +) + +type Github struct{} + +func (Github) ValidateProviderConfig(ctx *validation.Context, cfg oauthrelyingparty.ProviderConfig) { + ctx.AddError(Schema.Validator().ValidateValue(cfg)) +} + +func (Github) SetDefaults(cfg oauthrelyingparty.ProviderConfig) { + cfg.SetDefaultsModifyDisabledFalse() + cfg.SetDefaultsEmailClaimConfig(oauthrelyingpartyutil.Email_AssumeVerified_Required()) +} + +func (Github) ProviderID(cfg oauthrelyingparty.ProviderConfig) oauthrelyingparty.ProviderID { + // Github does NOT support OIDC. + // Github user ID is public, not scoped to anything. + return oauthrelyingparty.NewProviderID(cfg.Type(), nil) +} + +func (Github) scope() []string { + // https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps + return []string{"read:user", "user:email"} +} + +func (p Github) GetAuthorizationURL(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetAuthorizationURLOptions) (string, error) { + // https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#1-request-a-users-github-identity + return oauthrelyingpartyutil.MakeAuthorizationURL(githubAuthorizationURL, oauthrelyingpartyutil.AuthorizationURLParams{ + ClientID: deps.ProviderConfig.ClientID(), + RedirectURI: param.RedirectURI, + Scope: p.scope(), + // ResponseType is unset. + // ResponseMode is unset. + State: param.State, + // Prompt is unset. + // Nonce is unset. + }.Query()), nil +} + +func (p Github) GetUserProfile(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetUserProfileOptions) (authInfo oauthrelyingparty.UserProfile, err error) { + accessTokenResp, err := p.exchangeCode(deps, param) + if err != nil { + return + } + + userProfile, err := p.fetchUserInfo(deps, accessTokenResp) + if err != nil { + return + } + authInfo.ProviderRawProfile = userProfile + + idJSONNumber, _ := userProfile["id"].(json.Number) + email, _ := userProfile["email"].(string) + login, _ := userProfile["login"].(string) + picture, _ := userProfile["avatar_url"].(string) + profile, _ := userProfile["html_url"].(string) + + id := string(idJSONNumber) + + authInfo.ProviderUserID = id + emailRequired := deps.ProviderConfig.EmailClaimConfig().Required() + stdAttrs, err := stdattrs.Extract(map[string]interface{}{ + stdattrs.Email: email, + stdattrs.Name: login, + stdattrs.GivenName: login, + stdattrs.Picture: picture, + stdattrs.Profile: profile, + }, stdattrs.ExtractOptions{ + EmailRequired: emailRequired, + }) + if err != nil { + err = apierrors.AddDetails(err, errorutil.Details{ + "ProviderType": apierrors.APIErrorDetail.Value(deps.ProviderConfig.Type()), + }) + return + } + authInfo.StandardAttributes = stdAttrs + + return +} + +func (Github) exchangeCode(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetUserProfileOptions) (accessTokenResp oauthrelyingpartyutil.AccessTokenResp, err error) { + q := make(url.Values) + q.Set("client_id", deps.ProviderConfig.ClientID()) + q.Set("client_secret", deps.ClientSecret) + q.Set("code", param.Code) + q.Set("redirect_uri", param.RedirectURI) + + body := strings.NewReader(q.Encode()) + req, _ := http.NewRequest("POST", githubTokenURL, body) + // https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#2-users-are-redirected-back-to-your-site-by-github + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := deps.HTTPClient.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + err = json.NewDecoder(resp.Body).Decode(&accessTokenResp) + if err != nil { + return + } + } else { + var errResp oauthrelyingparty.ErrorResponse + err = json.NewDecoder(resp.Body).Decode(&errResp) + if err != nil { + return + } + err = oauthrelyingpartyutil.ErrorResponseAsError(errResp) + } + + return +} + +func (Github) fetchUserInfo(deps oauthrelyingparty.Dependencies, accessTokenResp oauthrelyingpartyutil.AccessTokenResp) (userProfile map[string]interface{}, err error) { + tokenType := accessTokenResp.TokenType() + accessTokenValue := accessTokenResp.AccessToken() + authorizationHeader := fmt.Sprintf("%s %s", tokenType, accessTokenValue) + + req, err := http.NewRequest(http.MethodGet, githubUserInfoURL, nil) + if err != nil { + return + } + req.Header.Add("Authorization", authorizationHeader) + + resp, err := deps.HTTPClient.Do(req) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return + } + + if resp.StatusCode != 200 { + err = fmt.Errorf("failed to fetch user profile: unexpected status code: %d", resp.StatusCode) + return + } + + decoder := json.NewDecoder(resp.Body) + // Deserialize "id" as json.Number. + decoder.UseNumber() + err = decoder.Decode(&userProfile) + if err != nil { + return + } + + return +} diff --git a/pkg/lib/authn/sso/github_test.go b/pkg/lib/oauthrelyingparty/github/provider_test.go similarity index 50% rename from pkg/lib/authn/sso/github_test.go rename to pkg/lib/oauthrelyingparty/github/provider_test.go index 458e2f3489..974eb0e481 100644 --- a/pkg/lib/authn/sso/github_test.go +++ b/pkg/lib/oauthrelyingparty/github/provider_test.go @@ -1,23 +1,24 @@ -package sso +package github import ( "testing" - "github.com/authgear/authgear-server/pkg/lib/config" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" ) -func TestGithubImpl(t *testing.T) { - Convey("GithubImpl", t, func() { - g := &GithubImpl{ - ProviderConfig: config.OAuthSSOProviderConfig{ - ClientID: "client_id", - Type: config.OAuthSSOProviderTypeGithub, +func TestGithub(t *testing.T) { + Convey("Github", t, func() { + deps := oauthrelyingparty.Dependencies{ + ProviderConfig: oauthrelyingparty.ProviderConfig{ + "client_id": "client_id", + "type": Type, }, - HTTPClient: OAuthHTTPClient{}, } + g := Github{} - u, err := g.GetAuthURL(GetAuthURLParam{ + u, err := g.GetAuthorizationURL(deps, oauthrelyingparty.GetAuthorizationURLOptions{ RedirectURI: "https://localhost/", Nonce: "nonce", State: "state", diff --git a/pkg/lib/oauthrelyingparty/google/provider.go b/pkg/lib/oauthrelyingparty/google/provider.go new file mode 100644 index 0000000000..a0cf9c15cc --- /dev/null +++ b/pkg/lib/oauthrelyingparty/google/provider.go @@ -0,0 +1,182 @@ +package google + +import ( + "context" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + + "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" + liboauthrelyingparty "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil" + "github.com/authgear/authgear-server/pkg/util/validation" +) + +func init() { + oauthrelyingparty.RegisterProvider(Type, Google{}) +} + +const Type = liboauthrelyingparty.TypeGoogle + +var _ oauthrelyingparty.Provider = Google{} +var _ liboauthrelyingparty.BuiltinProvider = Google{} + +var Schema = validation.NewSimpleSchema(` +{ + "type": "object", + "additionalProperties": false, + "properties": { + "alias": { "type": "string" }, + "type": { "type": "string" }, + "modify_disabled": { "type": "boolean" }, + "client_id": { "type": "string", "minLength": 1 }, + "claims": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "object", + "additionalProperties": false, + "properties": { + "assume_verified": { "type": "boolean" }, + "required": { "type": "boolean" } + } + } + } + } + }, + "required": ["alias", "type", "client_id"] +} +`) + +const ( + googleOIDCDiscoveryDocumentURL string = "https://accounts.google.com/.well-known/openid-configuration" +) + +type Google struct{} + +func (Google) ValidateProviderConfig(ctx *validation.Context, cfg oauthrelyingparty.ProviderConfig) { + ctx.AddError(Schema.Validator().ValidateValue(cfg)) +} + +func (Google) SetDefaults(cfg oauthrelyingparty.ProviderConfig) { + cfg.SetDefaultsModifyDisabledFalse() + cfg.SetDefaultsEmailClaimConfig(oauthrelyingpartyutil.Email_AssumeVerified_Required()) +} + +func (Google) ProviderID(cfg oauthrelyingparty.ProviderConfig) oauthrelyingparty.ProviderID { + // Google supports OIDC. + // sub is public, not scoped to anything so changing client_id does not affect sub. + // Therefore, ProviderID is simply the type. + // + // Rotating the OAuth application is OK. + return oauthrelyingparty.NewProviderID(cfg.Type(), nil) +} + +func (Google) scope() []string { + // https://developers.google.com/identity/protocols/oauth2/openid-connect + return []string{"openid", "profile", "email"} +} + +func (p Google) GetAuthorizationURL(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetAuthorizationURLOptions) (string, error) { + d, err := oauthrelyingpartyutil.FetchOIDCDiscoveryDocument(deps.HTTPClient, googleOIDCDiscoveryDocumentURL) + if err != nil { + return "", err + } + return d.MakeOAuthURL(oauthrelyingpartyutil.AuthorizationURLParams{ + ClientID: deps.ProviderConfig.ClientID(), + RedirectURI: param.RedirectURI, + Scope: p.scope(), + ResponseType: oauthrelyingparty.ResponseTypeCode, + ResponseMode: param.ResponseMode, + State: param.State, + Nonce: param.Nonce, + Prompt: p.getPrompt(param.Prompt), + }), nil +} + +func (Google) GetUserProfile(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetUserProfileOptions) (authInfo oauthrelyingparty.UserProfile, err error) { + d, err := oauthrelyingpartyutil.FetchOIDCDiscoveryDocument(deps.HTTPClient, googleOIDCDiscoveryDocumentURL) + if err != nil { + return + } + // OPTIMIZE(sso): Cache JWKs + keySet, err := d.FetchJWKs(deps.HTTPClient) + if err != nil { + return + } + + var tokenResp oauthrelyingpartyutil.AccessTokenResp + jwtToken, err := d.ExchangeCode( + deps.HTTPClient, + deps.Clock, + param.Code, + keySet, + deps.ProviderConfig.ClientID(), + deps.ClientSecret, + param.RedirectURI, + param.Nonce, + &tokenResp, + ) + if err != nil { + return + } + + claims, err := jwtToken.AsMap(context.TODO()) + if err != nil { + return + } + + // Verify the issuer + // https://developers.google.com/identity/protocols/OpenIDConnect#validatinganidtoken + iss, ok := claims["iss"].(string) + if !ok { + err = oauthrelyingpartyutil.OAuthProtocolError.New("iss not found in ID token") + return + } + if iss != "https://accounts.google.com" && iss != "accounts.google.com" { + err = oauthrelyingpartyutil.OAuthProtocolError.New("iss is not from Google") + return + } + + // Ensure sub exists + sub, ok := claims["sub"].(string) + if !ok { + err = oauthrelyingpartyutil.OAuthProtocolError.New("sub not found in ID token") + return + } + + authInfo.ProviderRawProfile = claims + authInfo.ProviderUserID = sub + // Google supports + // given_name, family_name, email, picture, profile, locale + // https://developers.google.com/identity/protocols/oauth2/openid-connect#obtainuserinfo + emailRequired := deps.ProviderConfig.EmailClaimConfig().Required() + stdAttrs, err := stdattrs.Extract(claims, stdattrs.ExtractOptions{ + EmailRequired: emailRequired, + }) + if err != nil { + return + } + authInfo.StandardAttributes = stdAttrs + + return +} + +func (Google) getPrompt(prompt []string) []string { + // Google supports `none`, `consent` and `select_account` for prompt. + // The usage of `none` is for checking existing authentication and/or consent + // which doesn't fit auth ui case. + // https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters + newPrompt := []string{} + for _, p := range prompt { + if p == "consent" || + p == "select_account" { + newPrompt = append(newPrompt, p) + } + } + if len(newPrompt) == 0 { + // default + return []string{"select_account"} + } + return newPrompt +} diff --git a/pkg/lib/authn/sso/google_test.go b/pkg/lib/oauthrelyingparty/google/provider_test.go similarity index 81% rename from pkg/lib/authn/sso/google_test.go rename to pkg/lib/oauthrelyingparty/google/provider_test.go index 0c62c515de..5a6d9f3b55 100644 --- a/pkg/lib/authn/sso/google_test.go +++ b/pkg/lib/oauthrelyingparty/google/provider_test.go @@ -1,28 +1,31 @@ -package sso +package google import ( "net/http" "testing" - "github.com/authgear/authgear-server/pkg/lib/config" . "github.com/smartystreets/goconvey/convey" "gopkg.in/h2non/gock.v1" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" ) func TestGoogleImpl(t *testing.T) { Convey("GoogleImpl", t, func() { - client := OAuthHTTPClient{&http.Client{}} - gock.InterceptClient(client.Client) + client := &http.Client{} + gock.InterceptClient(client) defer gock.Off() - g := &GoogleImpl{ - ProviderConfig: config.OAuthSSOProviderConfig{ - ClientID: "client_id", - Type: config.OAuthSSOProviderTypeGoogle, + deps := oauthrelyingparty.Dependencies{ + ProviderConfig: oauthrelyingparty.ProviderConfig{ + "client_id": "client_id", + "type": Type, }, HTTPClient: client, } + g := Google{} + gock.New(googleOIDCDiscoveryDocumentURL). Reply(200). BodyString(` @@ -87,9 +90,9 @@ func TestGoogleImpl(t *testing.T) { `) defer func() { gock.Flush() }() - u, err := g.GetAuthURL(GetAuthURLParam{ + u, err := g.GetAuthorizationURL(deps, oauthrelyingparty.GetAuthorizationURLOptions{ RedirectURI: "https://localhost/", - ResponseMode: ResponseModeFormPost, + ResponseMode: oauthrelyingparty.ResponseModeFormPost, Nonce: "nonce", State: "state", Prompt: []string{"login"}, diff --git a/pkg/lib/authn/sso/linkedin.go b/pkg/lib/oauthrelyingparty/linkedin/provider.go similarity index 80% rename from pkg/lib/authn/sso/linkedin.go rename to pkg/lib/oauthrelyingparty/linkedin/provider.go index eaba263c5c..1346811bed 100644 --- a/pkg/lib/authn/sso/linkedin.go +++ b/pkg/lib/oauthrelyingparty/linkedin/provider.go @@ -1,10 +1,51 @@ -package sso +package linkedin import ( + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" - "github.com/authgear/authgear-server/pkg/lib/config" + liboauthrelyingparty "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil" + "github.com/authgear/authgear-server/pkg/util/validation" ) +func init() { + oauthrelyingparty.RegisterProvider(Type, Linkedin{}) +} + +const Type = liboauthrelyingparty.TypeLinkedin + +var _ oauthrelyingparty.Provider = Linkedin{} +var _ liboauthrelyingparty.BuiltinProvider = Linkedin{} + +var Schema = validation.NewSimpleSchema(` +{ + "type": "object", + "additionalProperties": false, + "properties": { + "alias": { "type": "string" }, + "type": { "type": "string" }, + "modify_disabled": { "type": "boolean" }, + "client_id": { "type": "string", "minLength": 1 }, + "claims": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "object", + "additionalProperties": false, + "properties": { + "assume_verified": { "type": "boolean" }, + "required": { "type": "boolean" } + } + } + } + } + }, + "required": ["alias", "type", "client_id"] +} +`) + const ( linkedinAuthorizationURL string = "https://www.linkedin.com/oauth/v2/authorization" // nolint: gosec @@ -13,57 +54,70 @@ const ( linkedinContactURL string = "https://api.linkedin.com/v2/clientAwareMemberHandles?q=members&projection=(elements*(primary,type,handle~))" ) -type LinkedInImpl struct { - ProviderConfig config.OAuthSSOProviderConfig - Credentials config.OAuthSSOProviderCredentialsItem - StandardAttributesNormalizer StandardAttributesNormalizer - HTTPClient OAuthHTTPClient +type Linkedin struct{} + +func (Linkedin) ValidateProviderConfig(ctx *validation.Context, cfg oauthrelyingparty.ProviderConfig) { + ctx.AddError(Schema.Validator().ValidateValue(cfg)) +} + +func (Linkedin) SetDefaults(cfg oauthrelyingparty.ProviderConfig) { + cfg.SetDefaultsModifyDisabledFalse() + cfg.SetDefaultsEmailClaimConfig(oauthrelyingpartyutil.Email_AssumeVerified_Required()) } -func (*LinkedInImpl) Type() config.OAuthSSOProviderType { - return config.OAuthSSOProviderTypeLinkedIn +func (Linkedin) ProviderID(cfg oauthrelyingparty.ProviderConfig) oauthrelyingparty.ProviderID { + // Linkedin does NOT support OIDC. + // Linkedin user ID is scoped to client_id. + // Therefore, ProviderID is Type + client_id. + // + // Rotating the OAuth application is problematic. + keys := map[string]interface{}{ + "client_id": cfg.ClientID(), + } + return oauthrelyingparty.NewProviderID(cfg.Type(), keys) } -func (f *LinkedInImpl) Config() config.OAuthSSOProviderConfig { - return f.ProviderConfig +func (Linkedin) scope() []string { + // https://docs.microsoft.com/en-us/linkedin/shared/references/v2/profile/lite-profile + // https://docs.microsoft.com/en-us/linkedin/shared/integrations/people/primary-contact-api?context=linkedin/compliance/context + return []string{"r_liteprofile", "r_emailaddress"} } -func (f *LinkedInImpl) GetAuthURL(param GetAuthURLParam) (string, error) { - return MakeAuthorizationURL(linkedinAuthorizationURL, AuthorizationURLParams{ - ClientID: f.ProviderConfig.ClientID, +func (p Linkedin) GetAuthorizationURL(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetAuthorizationURLOptions) (string, error) { + return oauthrelyingpartyutil.MakeAuthorizationURL(linkedinAuthorizationURL, oauthrelyingpartyutil.AuthorizationURLParams{ + ClientID: deps.ProviderConfig.ClientID(), RedirectURI: param.RedirectURI, - Scope: f.ProviderConfig.Type.Scope(), - ResponseType: ResponseTypeCode, + Scope: p.scope(), + ResponseType: oauthrelyingparty.ResponseTypeCode, // ResponseMode is unset. - State: param.State, - Prompt: f.GetPrompt(param.Prompt), + State: param.State, + // Prompt is unset. + // Linkedin doesn't support prompt parameter + // https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?tabs=HTTPS#step-2-request-an-authorization-code + // Nonce is unset }.Query()), nil } -func (f *LinkedInImpl) GetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - return f.NonOpenIDConnectGetAuthInfo(r, param) -} - -func (f *LinkedInImpl) NonOpenIDConnectGetAuthInfo(r OAuthAuthorizationResponse, param GetAuthInfoParam) (authInfo AuthInfo, err error) { - accessTokenResp, err := fetchAccessTokenResp( - f.HTTPClient, - r.Code, +func (Linkedin) GetUserProfile(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetUserProfileOptions) (authInfo oauthrelyingparty.UserProfile, err error) { + accessTokenResp, err := oauthrelyingpartyutil.FetchAccessTokenResp( + deps.HTTPClient, + param.Code, linkedinTokenURL, param.RedirectURI, - f.ProviderConfig.ClientID, - f.Credentials.ClientSecret, + deps.ProviderConfig.ClientID(), + deps.ClientSecret, ) if err != nil { return } - meResponse, err := fetchUserProfile(f.HTTPClient, accessTokenResp, linkedinMeURL) + meResponse, err := oauthrelyingpartyutil.FetchUserProfile(deps.HTTPClient, accessTokenResp, linkedinMeURL) if err != nil { return } - contactResponse, err := fetchUserProfile(f.HTTPClient, accessTokenResp, linkedinContactURL) + contactResponse, err := oauthrelyingpartyutil.FetchUserProfile(deps.HTTPClient, accessTokenResp, linkedinContactURL) if err != nil { return } @@ -276,28 +330,18 @@ func (f *LinkedInImpl) NonOpenIDConnectGetAuthInfo(r OAuthAuthorizationResponse, id, attrs := decodeLinkedIn(combinedResponse) authInfo.ProviderUserID = id + emailRequired := deps.ProviderConfig.EmailClaimConfig().Required() attrs, err = stdattrs.Extract(attrs, stdattrs.ExtractOptions{ - EmailRequired: *f.ProviderConfig.Claims.Email.Required, + EmailRequired: emailRequired, }) if err != nil { return } authInfo.StandardAttributes = attrs - err = f.StandardAttributesNormalizer.Normalize(authInfo.StandardAttributes) - if err != nil { - return - } - return } -func (f *LinkedInImpl) GetPrompt(prompt []string) []string { - // linkedin doesn't support prompt parameter - // ref: https://docs.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?tabs=HTTPS#step-2-request-an-authorization-code - return []string{} -} - func decodeLinkedIn(userInfo map[string]interface{}) (string, stdattrs.T) { profile := userInfo["profile"].(map[string]interface{}) id := profile["id"].(string) @@ -352,8 +396,3 @@ func decodeLinkedIn(userInfo map[string]interface{}) (string, stdattrs.T) { stdattrs.Picture: picture, } } - -var ( - _ OAuthProvider = &LinkedInImpl{} - _ NonOpenIDConnectProvider = &LinkedInImpl{} -) diff --git a/pkg/lib/authn/sso/linkedin_test.go b/pkg/lib/oauthrelyingparty/linkedin/provider_test.go similarity index 52% rename from pkg/lib/authn/sso/linkedin_test.go rename to pkg/lib/oauthrelyingparty/linkedin/provider_test.go index 572afefb13..4710d1a336 100644 --- a/pkg/lib/authn/sso/linkedin_test.go +++ b/pkg/lib/oauthrelyingparty/linkedin/provider_test.go @@ -1,23 +1,24 @@ -package sso +package linkedin import ( "testing" - "github.com/authgear/authgear-server/pkg/lib/config" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" ) -func TestLinkedInImpl(t *testing.T) { - Convey("LinkedInImpl", t, func() { - g := &LinkedInImpl{ - ProviderConfig: config.OAuthSSOProviderConfig{ - ClientID: "client_id", - Type: config.OAuthSSOProviderTypeLinkedIn, +func TestLinkedin(t *testing.T) { + Convey("Linkedin", t, func() { + deps := oauthrelyingparty.Dependencies{ + ProviderConfig: oauthrelyingparty.ProviderConfig{ + "client_id": "client_id", + "type": Type, }, - HTTPClient: OAuthHTTPClient{}, } + g := Linkedin{} - u, err := g.GetAuthURL(GetAuthURLParam{ + u, err := g.GetAuthorizationURL(deps, oauthrelyingparty.GetAuthorizationURLOptions{ RedirectURI: "https://localhost/", Nonce: "nonce", State: "state", diff --git a/pkg/lib/authn/sso/access_token.go b/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/access_token.go similarity index 89% rename from pkg/lib/authn/sso/access_token.go rename to pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/access_token.go index 8bc607de7f..678f7afd7f 100644 --- a/pkg/lib/authn/sso/access_token.go +++ b/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/access_token.go @@ -1,10 +1,13 @@ -package sso +package oauthrelyingpartyutil import ( "encoding/json" + "net/http" "net/url" "strconv" "strings" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" ) type AccessTokenResp map[string]interface{} @@ -76,8 +79,8 @@ func (r AccessTokenResp) TokenType() string { } } -func fetchAccessTokenResp( - client OAuthHTTPClient, +func FetchAccessTokenResp( + client *http.Client, code string, accessTokenURL string, redirectURL string, @@ -106,12 +109,12 @@ func fetchAccessTokenResp( return } } else { // normally 400 Bad Request - var errResp oauthErrorResp + var errResp oauthrelyingparty.ErrorResponse err = json.NewDecoder(resp.Body).Decode(&errResp) if err != nil { return } - err = errResp.AsError() + err = ErrorResponseAsError(errResp) } return diff --git a/pkg/lib/authn/sso/authurl.go b/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/authorization_url.go similarity index 78% rename from pkg/lib/authn/sso/authurl.go rename to pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/authorization_url.go index b347d364ca..28be954eec 100644 --- a/pkg/lib/authn/sso/authurl.go +++ b/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/authorization_url.go @@ -1,29 +1,16 @@ -package sso +package oauthrelyingpartyutil import ( "net/url" "strings" ) -type ResponseType string - -const ( - ResponseTypeCode ResponseType = "code" -) - -type ResponseMode string - -const ( - ResponseModeFormPost ResponseMode = "form_post" - ResponseModeQuery ResponseMode = "query" -) - type AuthorizationURLParams struct { ClientID string RedirectURI string Scope []string - ResponseType ResponseType - ResponseMode ResponseMode + ResponseType string + ResponseMode string State string Prompt []string Nonce string diff --git a/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/claim_config.go b/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/claim_config.go new file mode 100644 index 0000000000..d811066859 --- /dev/null +++ b/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/claim_config.go @@ -0,0 +1,17 @@ +package oauthrelyingpartyutil + +import "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + +func Email_AssumeVerified_Required() oauthrelyingparty.ProviderClaimConfig { + return oauthrelyingparty.ProviderClaimConfig{ + "assume_verified": true, + "required": true, + } +} + +func Email_AssumeVerified_NOT_Required() oauthrelyingparty.ProviderClaimConfig { + return oauthrelyingparty.ProviderClaimConfig{ + "assume_verified": true, + "required": false, + } +} diff --git a/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/error.go b/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/error.go new file mode 100644 index 0000000000..6e18747844 --- /dev/null +++ b/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/error.go @@ -0,0 +1,28 @@ +package oauthrelyingpartyutil + +import ( + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + + "github.com/authgear/authgear-server/pkg/api/apierrors" +) + +var InvalidConfiguration = apierrors.InternalError.WithReason("InvalidConfiguration") +var OAuthProtocolError = apierrors.BadRequest.WithReason("OAuthProtocolError") +var OAuthError = apierrors.BadRequest.WithReason("OAuthError") + +func NewOAuthError(errorString string, errorDescription string, errorURI string) error { + msg := errorString + if errorDescription != "" { + msg += ": " + errorDescription + } + + return OAuthError.NewWithInfo(msg, apierrors.Details{ + "error": errorString, + "error_description": errorDescription, + "error_uri": errorURI, + }) +} + +func ErrorResponseAsError(errResp oauthrelyingparty.ErrorResponse) error { + return NewOAuthError(errResp.Error, errResp.ErrorDescription, errResp.ErrorURI) +} diff --git a/pkg/lib/authn/sso/oidc.go b/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/oidc.go similarity index 88% rename from pkg/lib/authn/sso/oidc.go rename to pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/oidc.go index 84c683b660..dca64c51eb 100644 --- a/pkg/lib/authn/sso/oidc.go +++ b/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/oidc.go @@ -1,4 +1,4 @@ -package sso +package oauthrelyingpartyutil import ( "crypto/subtle" @@ -11,13 +11,14 @@ import ( "github.com/lestrrat-go/jwx/v2/jwk" "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/authgear/authgear-server/pkg/util/clock" + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/util/duration" "github.com/authgear/authgear-server/pkg/util/jwsutil" ) type jwtClock struct { - Clock clock.Clock + Clock oauthrelyingparty.Clock } func (c jwtClock) Now() time.Time { @@ -31,7 +32,7 @@ type OIDCDiscoveryDocument struct { JWKSUri string `json:"jwks_uri"` } -func FetchOIDCDiscoveryDocument(client OAuthHTTPClient, endpoint string) (*OIDCDiscoveryDocument, error) { +func FetchOIDCDiscoveryDocument(client *http.Client, endpoint string) (*OIDCDiscoveryDocument, error) { resp, err := client.Get(endpoint) if resp != nil { defer resp.Body.Close() @@ -60,7 +61,7 @@ func (d *OIDCDiscoveryDocument) MakeOAuthURL(params AuthorizationURLParams) stri return MakeAuthorizationURL(d.AuthorizationEndpoint, params.Query()) } -func (d *OIDCDiscoveryDocument) FetchJWKs(client OAuthHTTPClient) (jwk.Set, error) { +func (d *OIDCDiscoveryDocument) FetchJWKs(client *http.Client) (jwk.Set, error) { resp, err := client.Get(d.JWKSUri) if resp != nil { defer resp.Body.Close() @@ -75,8 +76,8 @@ func (d *OIDCDiscoveryDocument) FetchJWKs(client OAuthHTTPClient) (jwk.Set, erro } func (d *OIDCDiscoveryDocument) ExchangeCode( - client OAuthHTTPClient, - clock clock.Clock, + client *http.Client, + clock oauthrelyingparty.Clock, code string, jwks jwk.Set, clientID string, @@ -104,12 +105,12 @@ func (d *OIDCDiscoveryDocument) ExchangeCode( return nil, err } } else { - var errorResp oauthErrorResp + var errorResp oauthrelyingparty.ErrorResponse err = json.NewDecoder(resp.Body).Decode(&errorResp) if err != nil { return nil, err } - err = errorResp.AsError() + err = ErrorResponseAsError(errorResp) return nil, err } diff --git a/pkg/lib/authn/sso/user_profile.go b/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/user_profile.go similarity index 91% rename from pkg/lib/authn/sso/user_profile.go rename to pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/user_profile.go index e08a3744ef..8aea2da4ee 100644 --- a/pkg/lib/authn/sso/user_profile.go +++ b/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil/user_profile.go @@ -1,4 +1,4 @@ -package sso +package oauthrelyingpartyutil import ( "encoding/json" @@ -6,8 +6,8 @@ import ( "net/http" ) -func fetchUserProfile( - client OAuthHTTPClient, +func FetchUserProfile( + client *http.Client, accessTokenResp AccessTokenResp, userProfileURL string, ) (userProfile map[string]interface{}, err error) { diff --git a/pkg/lib/oauthrelyingparty/wechat/provider.go b/pkg/lib/oauthrelyingparty/wechat/provider.go new file mode 100644 index 0000000000..54ebd38212 --- /dev/null +++ b/pkg/lib/oauthrelyingparty/wechat/provider.go @@ -0,0 +1,365 @@ +package wechat + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" + + "github.com/authgear/authgear-server/pkg/lib/authn/stdattrs" + liboauthrelyingparty "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty" + "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/oauthrelyingpartyutil" + "github.com/authgear/authgear-server/pkg/util/validation" +) + +func init() { + oauthrelyingparty.RegisterProvider(Type, Wechat{}) +} + +type AppType string + +const ( + AppTypeWeb AppType = "web" + AppTypeMobile AppType = "mobile" +) + +type ProviderConfig oauthrelyingparty.ProviderConfig + +func (c ProviderConfig) AppType() AppType { + app_type, _ := c["app_type"].(string) + return AppType(app_type) +} + +func (c ProviderConfig) AccountID() string { + account_id, _ := c["account_id"].(string) + return account_id +} + +func (c ProviderConfig) IsSandboxAccount() bool { + is_sandbox_account, _ := c["is_sandbox_account"].(bool) + return is_sandbox_account +} + +func (c ProviderConfig) WechatRedirectURIs() []string { + var out []string + wechat_redirect_uris, _ := c["wechat_redirect_uris"].([]interface{}) + for _, iface := range wechat_redirect_uris { + if s, ok := iface.(string); ok { + out = append(out, s) + } + } + return out +} + +const Type = liboauthrelyingparty.TypeWechat + +var _ oauthrelyingparty.Provider = Wechat{} +var _ liboauthrelyingparty.BuiltinProvider = Wechat{} + +var Schema = validation.NewSimpleSchema(` +{ + "type": "object", + "additionalProperties": false, + "properties": { + "alias": { "type": "string" }, + "type": { "type": "string" }, + "modify_disabled": { "type": "boolean" }, + "client_id": { "type": "string", "minLength": 1 }, + "claims": { + "type": "object", + "additionalProperties": false, + "properties": { + "email": { + "type": "object", + "additionalProperties": false, + "properties": { + "assume_verified": { "type": "boolean" }, + "required": { "type": "boolean" } + } + } + } + }, + "app_type": { "type": "string", "enum": ["mobile", "web"] }, + "account_id": { "type": "string", "format": "wechat_account_id" }, + "is_sandbox_account": { "type": "boolean" }, + "wechat_redirect_uris": { "type": "array", "items": { "type": "string", "format": "uri" } } + }, + "required": ["alias", "type", "client_id", "app_type", "account_id"] +} +`) + +const ( + wechatAuthorizationURL = "https://open.weixin.qq.com/connect/oauth2/authorize" + // nolint: gosec + wechatAccessTokenURL = "https://api.weixin.qq.com/sns/oauth2/access_token" + wechatUserInfoURL = "https://api.weixin.qq.com/sns/userinfo" +) + +type Wechat struct{} + +func (Wechat) ValidateProviderConfig(ctx *validation.Context, cfg oauthrelyingparty.ProviderConfig) { + ctx.AddError(Schema.Validator().ValidateValue(cfg)) +} + +func (Wechat) SetDefaults(cfg oauthrelyingparty.ProviderConfig) { + cfg.SetDefaultsModifyDisabledFalse() + cfg.SetDefaultsEmailClaimConfig(oauthrelyingpartyutil.Email_AssumeVerified_NOT_Required()) +} + +func (Wechat) ProviderID(cfg oauthrelyingparty.ProviderConfig) oauthrelyingparty.ProviderID { + // WeChat does NOT support OIDC. + // In the same Weixin Open Platform account, the user UnionID is unique. + // The id is scoped to Open Platform account. + // https://developers.weixin.qq.com/miniprogram/en/dev/framework/open-ability/union-id.html + + wechatCfg := ProviderConfig(cfg) + account_id := wechatCfg.AccountID() + is_sandbox_account := wechatCfg.IsSandboxAccount() + keys := map[string]interface{}{ + "account_id": account_id, + "is_sandbox_account": strconv.FormatBool(is_sandbox_account), + } + + return oauthrelyingparty.NewProviderID(cfg.Type(), keys) +} + +func (Wechat) scope() []string { + // https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html + return []string{"snsapi_userinfo"} +} + +func (p Wechat) GetAuthorizationURL(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetAuthorizationURLOptions) (string, error) { + return oauthrelyingpartyutil.MakeAuthorizationURL(wechatAuthorizationURL, oauthrelyingpartyutil.AuthorizationURLParams{ + // ClientID is not used by wechat. + WechatAppID: deps.ProviderConfig.ClientID(), + RedirectURI: param.RedirectURI, + Scope: p.scope(), + ResponseType: oauthrelyingparty.ResponseTypeCode, + // ResponseMode is unset. + State: param.State, + // Prompt is unset. + // Wechat doesn't support prompt parameter + // https://developers.weixin.qq.com/doc/oplatform/en/Third-party_Platforms/Official_Accounts/official_account_website_authorization.html + // Nonce is unset. + }.Query()), nil +} + +func (Wechat) GetUserProfile(deps oauthrelyingparty.Dependencies, param oauthrelyingparty.GetUserProfileOptions) (authInfo oauthrelyingparty.UserProfile, err error) { + accessTokenResp, err := wechatFetchAccessTokenResp( + deps.HTTPClient, + param.Code, + deps.ProviderConfig.ClientID(), + deps.ClientSecret, + ) + if err != nil { + return + } + + rawProfile, err := wechatFetchUserProfile(deps.HTTPClient, accessTokenResp) + if err != nil { + return + } + + is_sandbox_account := ProviderConfig(deps.ProviderConfig).IsSandboxAccount() + var userID string + if is_sandbox_account { + if accessTokenResp.UnionID() != "" { + err = oauthrelyingpartyutil.InvalidConfiguration.New("invalid is_sandbox_account config, WeChat sandbox account should not have union id") + return + } + userID = accessTokenResp.OpenID() + } else { + userID = accessTokenResp.UnionID() + } + + if userID == "" { + // this may happen if developer misconfigure is_sandbox_account, e.g. sandbox account doesn't have union id + err = oauthrelyingpartyutil.InvalidConfiguration.New("invalid is_sandbox_account config, missing user id in wechat token response") + return + } + + // https://developers.weixin.qq.com/doc/offiaccount/User_Management/Get_users_basic_information_UnionID.html + // Here is an example of how the raw profile looks like. + // { + // "sex": 0, + // "city": "", + // "openid": "redacted", + // "country": "", + // "language": "zh_CN", + // "nickname": "John Doe", + // "province": "", + // "privilege": [], + // "headimgurl": "" + // } + var gender string + if sex, ok := rawProfile["sex"].(float64); ok { + if sex == 1 { + gender = "male" + } else if sex == 2 { + gender = "female" + } + } + + name, _ := rawProfile["nickname"].(string) + locale, _ := rawProfile["language"].(string) + + authInfo.ProviderRawProfile = rawProfile + authInfo.ProviderUserID = userID + + // Claims.Email.Required is not respected because wechat does not return the email claim. + authInfo.StandardAttributes = stdattrs.T{ + stdattrs.Name: name, + stdattrs.Locale: locale, + stdattrs.Gender: gender, + }.WithNameCopiedToGivenName() + + return +} + +type wechatOAuthErrorResp struct { + ErrorCode int `json:"errcode"` + ErrorMsg string `json:"errmsg"` +} + +func (r *wechatOAuthErrorResp) AsError() error { + return fmt.Errorf("wechat: %d: %s", r.ErrorCode, r.ErrorMsg) +} + +type wechatAccessTokenResp map[string]interface{} + +func (r wechatAccessTokenResp) AccessToken() string { + accessToken, ok := r["access_token"].(string) + if ok { + return accessToken + } + return "" +} + +func (r wechatAccessTokenResp) OpenID() string { + openid, ok := r["openid"].(string) + if ok { + return openid + } + return "" +} + +func (r wechatAccessTokenResp) UnionID() string { + unionid, ok := r["unionid"].(string) + if ok { + return unionid + } + return "" +} + +type wechatUserInfoResp map[string]interface{} + +func (r wechatUserInfoResp) OpenID() string { + openid, ok := r["openid"].(string) + if ok { + return openid + } + return "" +} + +func wechatFetchAccessTokenResp( + client *http.Client, + code string, + appid string, + secret string, +) (r wechatAccessTokenResp, err error) { + v := url.Values{} + v.Set("grant_type", "authorization_code") + v.Add("code", code) + v.Add("appid", appid) + v.Add("secret", secret) + + resp, err := client.PostForm(wechatAccessTokenURL, v) + if resp != nil { + defer resp.Body.Close() + } + + if err != nil { + return + } + + // wechat always return 200 + // to know if there is error, we need to parse the response body + if resp.StatusCode != 200 { + err = fmt.Errorf("wechat: unexpected status code: %d", resp.StatusCode) + return + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return + } + + err = json.NewDecoder(bytes.NewReader(body)).Decode(&r) + if err != nil { + return + } + if r.AccessToken() == "" { + // failed to obtain access token, parse the error response + var errResp wechatOAuthErrorResp + err = json.NewDecoder(bytes.NewReader(body)).Decode(&errResp) + if err != nil { + return + } + err = errResp.AsError() + return + } + return +} + +func wechatFetchUserProfile( + client *http.Client, + accessTokenResp wechatAccessTokenResp, +) (userProfile wechatUserInfoResp, err error) { + v := url.Values{} + v.Set("openid", accessTokenResp.OpenID()) + v.Set("access_token", accessTokenResp.AccessToken()) + v.Set("lang", "en") + + resp, err := client.PostForm(wechatUserInfoURL, v) + if resp != nil { + defer resp.Body.Close() + } + + if err != nil { + return + } + + // wechat always return 200 + // to know if there is error, we need to parse the response body + if resp.StatusCode != 200 { + err = fmt.Errorf("wechat: unexpected status code: %d", resp.StatusCode) + return + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return + } + + err = json.NewDecoder(bytes.NewReader(body)).Decode(&userProfile) + if err != nil { + return + } + if userProfile.OpenID() == "" { + // failed to obtain id from user info, parse the error response + var errResp wechatOAuthErrorResp + err = json.NewDecoder(bytes.NewReader(body)).Decode(&errResp) + if err != nil { + return + } + err = errResp.AsError() + return + } + + return +} diff --git a/pkg/lib/oauthrelyingparty/wechat/provider_test.go b/pkg/lib/oauthrelyingparty/wechat/provider_test.go new file mode 100644 index 0000000000..452e5281c4 --- /dev/null +++ b/pkg/lib/oauthrelyingparty/wechat/provider_test.go @@ -0,0 +1,30 @@ +package wechat + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/oauthrelyingparty/pkg/api/oauthrelyingparty" +) + +func TestWechat(t *testing.T) { + Convey("Wechat", t, func() { + deps := oauthrelyingparty.Dependencies{ + ProviderConfig: oauthrelyingparty.ProviderConfig{ + "client_id": "client_id", + "type": Type, + }, + } + + g := Wechat{} + + u, err := g.GetAuthorizationURL(deps, oauthrelyingparty.GetAuthorizationURLOptions{ + Nonce: "nonce", + State: "state", + Prompt: []string{"login"}, + }) + So(err, ShouldBeNil) + So(u, ShouldEqual, "https://open.weixin.qq.com/connect/oauth2/authorize?appid=client_id&redirect_uri=&response_type=code&scope=snsapi_userinfo&state=state") + }) +} diff --git a/pkg/lib/rolesgroups/store_group_user.go b/pkg/lib/rolesgroups/store_group_user.go index 844e61c5bd..96e0425448 100644 --- a/pkg/lib/rolesgroups/store_group_user.go +++ b/pkg/lib/rolesgroups/store_group_user.go @@ -1,12 +1,13 @@ package rolesgroups import ( + "github.com/lib/pq" + "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/lib/infra/db" "github.com/authgear/authgear-server/pkg/util/graphqlutil" "github.com/authgear/authgear-server/pkg/util/slice" "github.com/authgear/authgear-server/pkg/util/uuid" - "github.com/lib/pq" ) func (s *Store) ListGroupsByUserIDs(userIDs []string) (map[string][]*Group, error) { diff --git a/pkg/lib/rolesgroups/store_role_user.go b/pkg/lib/rolesgroups/store_role_user.go index 40dbe485e2..890bbb4521 100644 --- a/pkg/lib/rolesgroups/store_role_user.go +++ b/pkg/lib/rolesgroups/store_role_user.go @@ -3,13 +3,14 @@ package rolesgroups import ( "sort" + "github.com/lib/pq" + "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/lib/infra/db" "github.com/authgear/authgear-server/pkg/util/graphqlutil" "github.com/authgear/authgear-server/pkg/util/setutil" "github.com/authgear/authgear-server/pkg/util/slice" "github.com/authgear/authgear-server/pkg/util/uuid" - "github.com/lib/pq" ) func (s *Store) ListRolesByUserIDs(userIDs []string) (map[string][]*Role, error) { diff --git a/pkg/lib/rolesgroups/store_user.go b/pkg/lib/rolesgroups/store_user.go index 228563d33e..5bce2d4f38 100644 --- a/pkg/lib/rolesgroups/store_user.go +++ b/pkg/lib/rolesgroups/store_user.go @@ -4,9 +4,10 @@ import ( "database/sql" "errors" + "github.com/lib/pq" + "github.com/authgear/authgear-server/pkg/api" "github.com/authgear/authgear-server/pkg/lib/infra/db" - "github.com/lib/pq" ) func (s *Store) scanUserID(scanner db.Scanner) (string, error) { diff --git a/pkg/lib/sessionlisting/listing_test.go b/pkg/lib/sessionlisting/listing_test.go index 54689f92f6..3ff5d78ef9 100644 --- a/pkg/lib/sessionlisting/listing_test.go +++ b/pkg/lib/sessionlisting/listing_test.go @@ -4,14 +4,15 @@ import ( "testing" "time" + gomock "github.com/golang/mock/gomock" + . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/oauth" "github.com/authgear/authgear-server/pkg/lib/session" "github.com/authgear/authgear-server/pkg/lib/session/access" "github.com/authgear/authgear-server/pkg/lib/session/idpsession" "github.com/authgear/authgear-server/pkg/lib/sessionlisting" - gomock "github.com/golang/mock/gomock" - . "github.com/smartystreets/goconvey/convey" ) func makeDeviceInfo(deviceName string, deviceModel string) map[string]interface{} { diff --git a/pkg/lib/tutorial/service_test.go b/pkg/lib/tutorial/service_test.go index 9a9030ca29..83ec8164a4 100644 --- a/pkg/lib/tutorial/service_test.go +++ b/pkg/lib/tutorial/service_test.go @@ -5,6 +5,7 @@ import ( . "github.com/smartystreets/goconvey/convey" + _ "github.com/authgear/authgear-server/pkg/lib/oauthrelyingparty/google" "github.com/authgear/authgear-server/pkg/util/resource" ) diff --git a/pkg/lib/web/embedded_resource_test.go b/pkg/lib/web/embedded_resource_test.go index b28b2949bc..7526b3af48 100644 --- a/pkg/lib/web/embedded_resource_test.go +++ b/pkg/lib/web/embedded_resource_test.go @@ -7,8 +7,9 @@ import ( "testing" "time" - "github.com/authgear/authgear-server/pkg/lib/web" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/authgear-server/pkg/lib/web" ) func TestGlobalEmbeddedResourceManager(t *testing.T) { diff --git a/pkg/lib/workflow/event_store.go b/pkg/lib/workflow/event_store.go index 2a2f6d16c6..13affd763c 100644 --- a/pkg/lib/workflow/event_store.go +++ b/pkg/lib/workflow/event_store.go @@ -5,10 +5,11 @@ import ( "errors" "fmt" + goredis "github.com/go-redis/redis/v8" + "github.com/authgear/authgear-server/pkg/lib/config" "github.com/authgear/authgear-server/pkg/lib/infra/redis/appredis" "github.com/authgear/authgear-server/pkg/util/pubsub" - goredis "github.com/go-redis/redis/v8" ) type eventRedisPool struct{ *appredis.Handle } diff --git a/pkg/portal/appresource/util.go b/pkg/portal/appresource/util.go index 6d70154911..0beaf4ec91 100644 --- a/pkg/portal/appresource/util.go +++ b/pkg/portal/appresource/util.go @@ -4,8 +4,9 @@ import ( "io/ioutil" "path" - "github.com/authgear/authgear-server/pkg/util/resource" "github.com/spf13/afero" + + "github.com/authgear/authgear-server/pkg/util/resource" ) func cloneFS(fs resource.Fs) (afero.Fs, error) { diff --git a/pkg/portal/graphql/billing.go b/pkg/portal/graphql/billing.go index cb8ab1e981..b95df0be14 100644 --- a/pkg/portal/graphql/billing.go +++ b/pkg/portal/graphql/billing.go @@ -1,8 +1,9 @@ package graphql import ( - "github.com/authgear/authgear-server/pkg/portal/model" "github.com/graphql-go/graphql" + + "github.com/authgear/authgear-server/pkg/portal/model" ) var priceType = graphql.NewEnum(graphql.EnumConfig{ diff --git a/pkg/portal/graphql/billing_mutation.go b/pkg/portal/graphql/billing_mutation.go index cc159d2f7a..f5edfb1549 100644 --- a/pkg/portal/graphql/billing_mutation.go +++ b/pkg/portal/graphql/billing_mutation.go @@ -3,14 +3,15 @@ package graphql import ( "errors" + relay "github.com/authgear/graphql-go-relay" + "github.com/graphql-go/graphql" + "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/api/event/nonblocking" "github.com/authgear/authgear-server/pkg/portal/model" "github.com/authgear/authgear-server/pkg/portal/service" "github.com/authgear/authgear-server/pkg/portal/session" "github.com/authgear/authgear-server/pkg/util/graphqlutil" - relay "github.com/authgear/graphql-go-relay" - "github.com/graphql-go/graphql" ) var createCheckoutSessionInput = graphql.NewInputObject(graphql.InputObjectConfig{ diff --git a/pkg/portal/graphql/chart.go b/pkg/portal/graphql/chart.go index 76c4617edb..6127c0c2d0 100644 --- a/pkg/portal/graphql/chart.go +++ b/pkg/portal/graphql/chart.go @@ -4,10 +4,11 @@ import ( "errors" "time" - "github.com/authgear/authgear-server/pkg/api/apierrors" - "github.com/authgear/authgear-server/pkg/util/graphqlutil" relay "github.com/authgear/graphql-go-relay" "github.com/graphql-go/graphql" + + "github.com/authgear/authgear-server/pkg/api/apierrors" + "github.com/authgear/authgear-server/pkg/util/graphqlutil" ) var periodicalEnum = graphql.NewEnum(graphql.EnumConfig{ diff --git a/pkg/portal/graphql/context.go b/pkg/portal/graphql/context.go index b27362e625..249247bfd0 100644 --- a/pkg/portal/graphql/context.go +++ b/pkg/portal/graphql/context.go @@ -5,6 +5,8 @@ import ( "net/http" "time" + "github.com/stripe/stripe-go/v72" + "github.com/authgear/authgear-server/pkg/api/event" apimodel "github.com/authgear/authgear-server/pkg/api/model" "github.com/authgear/authgear-server/pkg/lib/analytic" @@ -19,7 +21,6 @@ import ( "github.com/authgear/authgear-server/pkg/util/graphqlutil" "github.com/authgear/authgear-server/pkg/util/log" "github.com/authgear/authgear-server/pkg/util/web3" - "github.com/stripe/stripe-go/v72" ) type UserLoader interface { diff --git a/pkg/portal/graphql/nodes.go b/pkg/portal/graphql/nodes.go index 957fe61f7d..5bb51bf708 100644 --- a/pkg/portal/graphql/nodes.go +++ b/pkg/portal/graphql/nodes.go @@ -5,9 +5,10 @@ import ( "fmt" "reflect" - "github.com/authgear/authgear-server/pkg/portal/session" relay "github.com/authgear/graphql-go-relay" "github.com/graphql-go/graphql" + + "github.com/authgear/authgear-server/pkg/portal/session" ) type NodeResolver func(ctx context.Context, id string) (interface{}, error) diff --git a/pkg/portal/service/kubernetes.go b/pkg/portal/service/kubernetes.go index df04396272..3c78a1d709 100644 --- a/pkg/portal/service/kubernetes.go +++ b/pkg/portal/service/kubernetes.go @@ -24,10 +24,11 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" + certmanagerclientset "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned" + portalconfig "github.com/authgear/authgear-server/pkg/portal/config" "github.com/authgear/authgear-server/pkg/util/kubeutil" "github.com/authgear/authgear-server/pkg/util/log" - certmanagerclientset "github.com/cert-manager/cert-manager/pkg/client/clientset/versioned" ) var LabelAppID = "authgear.com/app-id" diff --git a/pkg/portal/service/subscription.go b/pkg/portal/service/subscription.go index 652f82a715..dd835f0dc8 100644 --- a/pkg/portal/service/subscription.go +++ b/pkg/portal/service/subscription.go @@ -8,6 +8,7 @@ import ( "sigs.k8s.io/yaml" "github.com/Masterminds/squirrel" + "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/lib/config/configsource" "github.com/authgear/authgear-server/pkg/lib/infra/db/globaldb" diff --git a/pkg/util/hexstring/hexstring_test.go b/pkg/util/hexstring/hexstring_test.go index 48766bc2c2..6bb486bfce 100644 --- a/pkg/util/hexstring/hexstring_test.go +++ b/pkg/util/hexstring/hexstring_test.go @@ -4,8 +4,9 @@ import ( "math/big" "testing" - "github.com/authgear/authgear-server/pkg/util/hexstring" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/authgear-server/pkg/util/hexstring" ) func TestHexCmp(t *testing.T) { diff --git a/pkg/util/httpsigning/httpsigning_test.go b/pkg/util/httpsigning/httpsigning_test.go index 3794764416..507b191154 100644 --- a/pkg/util/httpsigning/httpsigning_test.go +++ b/pkg/util/httpsigning/httpsigning_test.go @@ -5,8 +5,9 @@ import ( "testing" "time" - "github.com/authgear/authgear-server/pkg/util/httputil" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/authgear-server/pkg/util/httputil" ) func TestHTTPSigning(t *testing.T) { diff --git a/pkg/util/httputil/json_test.go b/pkg/util/httputil/json_test.go index 66ab28a30e..5b48dd7c55 100644 --- a/pkg/util/httputil/json_test.go +++ b/pkg/util/httputil/json_test.go @@ -6,9 +6,10 @@ import ( "strings" "testing" + . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/authgear-server/pkg/api/apierrors" "github.com/authgear/authgear-server/pkg/util/validation" - . "github.com/smartystreets/goconvey/convey" ) func TestBindJSONBody(t *testing.T) { diff --git a/pkg/util/matchlist/matchlist_test.go b/pkg/util/matchlist/matchlist_test.go index c630450bbe..5721fec2ed 100644 --- a/pkg/util/matchlist/matchlist_test.go +++ b/pkg/util/matchlist/matchlist_test.go @@ -3,8 +3,9 @@ package matchlist_test import ( "testing" - "github.com/authgear/authgear-server/pkg/util/matchlist" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/authgear-server/pkg/util/matchlist" ) func TestMatchList(t *testing.T) { diff --git a/pkg/util/periodical/periodical_test.go b/pkg/util/periodical/periodical_test.go index b217891289..768cb7445d 100644 --- a/pkg/util/periodical/periodical_test.go +++ b/pkg/util/periodical/periodical_test.go @@ -4,9 +4,10 @@ import ( "testing" "time" + . "github.com/smartystreets/goconvey/convey" + "github.com/authgear/authgear-server/pkg/util/clock" "github.com/authgear/authgear-server/pkg/util/periodical" - . "github.com/smartystreets/goconvey/convey" ) func TestPeriodicalArgumentParser(t *testing.T) { diff --git a/pkg/util/timeutil/isoweek_test.go b/pkg/util/timeutil/isoweek_test.go index 91f35df87f..84966c89b7 100644 --- a/pkg/util/timeutil/isoweek_test.go +++ b/pkg/util/timeutil/isoweek_test.go @@ -4,8 +4,9 @@ import ( "testing" "time" - "github.com/authgear/authgear-server/pkg/util/timeutil" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/authgear-server/pkg/util/timeutil" ) func TestFirstDayOfISOWeek(t *testing.T) { diff --git a/pkg/util/timeutil/truncate_test.go b/pkg/util/timeutil/truncate_test.go index a3038fb225..f099d9588b 100644 --- a/pkg/util/timeutil/truncate_test.go +++ b/pkg/util/timeutil/truncate_test.go @@ -4,8 +4,9 @@ import ( "testing" "time" - "github.com/authgear/authgear-server/pkg/util/timeutil" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/authgear-server/pkg/util/timeutil" ) func TestDateUtil(t *testing.T) { diff --git a/pkg/util/web3/contractid_test.go b/pkg/util/web3/contractid_test.go index 193440845a..fa039d3731 100644 --- a/pkg/util/web3/contractid_test.go +++ b/pkg/util/web3/contractid_test.go @@ -5,8 +5,9 @@ import ( "net/url" "testing" - "github.com/authgear/authgear-server/pkg/util/web3" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/authgear-server/pkg/util/web3" ) func TestNew(t *testing.T) { diff --git a/pkg/util/web3/eip55.go b/pkg/util/web3/eip55.go index 7bae0fa1a0..ade5f3eea9 100644 --- a/pkg/util/web3/eip55.go +++ b/pkg/util/web3/eip55.go @@ -4,8 +4,9 @@ import ( "fmt" "regexp" - "github.com/authgear/authgear-server/pkg/util/hexstring" "github.com/ethereum/go-ethereum/common" + + "github.com/authgear/authgear-server/pkg/util/hexstring" ) // https://eips.ethereum.org/EIPS/eip-55 diff --git a/pkg/util/web3/eip681_test.go b/pkg/util/web3/eip681_test.go index 0f10ead800..4c1ef3b032 100644 --- a/pkg/util/web3/eip681_test.go +++ b/pkg/util/web3/eip681_test.go @@ -5,8 +5,9 @@ import ( "net/url" "testing" - "github.com/authgear/authgear-server/pkg/util/web3" . "github.com/smartystreets/goconvey/convey" + + "github.com/authgear/authgear-server/pkg/util/web3" ) func TestEIP681(t *testing.T) {