From 05e6e9ea76ef1ca9f6e2a1ea4e7ff348ef66dc97 Mon Sep 17 00:00:00 2001 From: Jonathan Reyes Date: Tue, 26 Mar 2024 11:56:42 -0600 Subject: [PATCH] feat: adding an OAuth provider for WorkOS --- .schemastore/config.schema.json | 32 ++++++- embedx/config.schema.json | 30 ++++++- go.mod | 5 +- go.sum | 12 ++- selfservice/strategy/oidc/provider_config.go | 5 ++ selfservice/strategy/oidc/provider_workos.go | 86 +++++++++++++++++++ .../strategy/oidc/provider_workos_test.go | 35 ++++++++ 7 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 selfservice/strategy/oidc/provider_workos.go create mode 100644 selfservice/strategy/oidc/provider_workos_test.go diff --git a/.schemastore/config.schema.json b/.schemastore/config.schema.json index 829b71bae3fb..aeca5deb9bed 100644 --- a/.schemastore/config.schema.json +++ b/.schemastore/config.schema.json @@ -415,7 +415,7 @@ }, "provider": { "title": "Provider", - "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon.", + "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon, workos.", "type": "string", "enum": [ "github", @@ -436,7 +436,8 @@ "dingtalk", "patreon", "linkedin", - "lark" + "lark", + "workos" ], "examples": ["google"] }, @@ -509,6 +510,12 @@ "type": "string", "examples": ["KP76DQS54M"] }, + "workos_organization_id": { + "title": "WorkOS Organization ID", + "description": "WorkOS Organization ID needed for performing SSO authorizations", + "type": "string", + "examples": ["org_000000000000000000000"] + }, "apple_private_key_id": { "title": "Apple Private Key Identifier", "description": "Sign In with Apple Private Key Identifier needed for generating a JWT token for client secret", @@ -636,6 +643,27 @@ } ] } + }, + { + "if": { + "properties": { + "provider": { + "const": "workos" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["workos_organization_id"] + }, + "else": { + "not": { + "properties": { + "workos_organization_id": {} + }, + "required": ["workos_organization_id"] + } + } } ] }, diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 15cc950fbf8e..78341640b51e 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -425,7 +425,7 @@ }, "provider": { "title": "Provider", - "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon.", + "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, slack, facebook, auth0, vk, yandex, apple, spotify, netid, dingtalk, patreon, workos.", "type": "string", "enum": [ "github", @@ -448,6 +448,7 @@ "linkedin", "linkedin_v2", "lark", + "workos", "x" ], "examples": ["google"] @@ -521,6 +522,12 @@ "type": "string", "examples": ["KP76DQS54M"] }, + "workos_organization_id": { + "title": "WorkOS Organization ID", + "description": "WorkOS Organization ID needed for performing SSO authorizations", + "type": "string", + "examples": ["org_000000000000000000000"] + }, "apple_private_key_id": { "title": "Apple Private Key Identifier", "description": "Sign In with Apple Private Key Identifier needed for generating a JWT token for client secret", @@ -648,6 +655,27 @@ } ] } + }, + { + "if": { + "properties": { + "provider": { + "const": "workos" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["workos_organization_id"] + }, + "else": { + "not": { + "properties": { + "workos_organization_id": {} + }, + "required": ["workos_organization_id"] + } + } } ] }, diff --git a/go.mod b/go.mod index 655a07bbf9e2..193bc978668f 100644 --- a/go.mod +++ b/go.mod @@ -180,7 +180,7 @@ require ( github.com/golang/glog v1.1.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.0.1 // indirect - github.com/google/go-querystring v1.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-tpm v0.9.0 // indirect github.com/google/pprof v0.0.0-20221010195024-131d412537ea // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect @@ -332,11 +332,14 @@ require ( github.com/coreos/go-oidc/v3 v3.9.0 github.com/dghubble/oauth1 v0.7.2 github.com/lestrrat-go/jwx/v2 v2.0.19 + github.com/motemen/go-loghttp v0.0.0-20231107055348-29ae44b293f4 + github.com/workos/workos-go/v3 v3.2.0 ) require ( github.com/jackc/puddle/v2 v2.1.2 // indirect github.com/lestrrat-go/httprc v1.0.4 // indirect + github.com/motemen/go-nuts v0.0.0-20220604134737-2658d0104f31 // indirect github.com/segmentio/asm v1.2.0 // indirect go.uber.org/atomic v1.10.0 // indirect ) diff --git a/go.sum b/go.sum index f344856176b4..98a364854b8f 100644 --- a/go.sum +++ b/go.sum @@ -370,6 +370,7 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -415,8 +416,9 @@ github.com/google/go-github/v38 v38.1.0 h1:C6h1FkaITcBFK7gAmq4eFzt6gbhEhk7L5z6R3 github.com/google/go-github/v38 v38.1.0/go.mod h1:cStvrz/7nFr0FoENgG6GLbp53WaelXucT+BBz/3VKx4= github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g= github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-tpm v0.9.0 h1:sQF6YqWMi+SCXpsmS3fd21oPy/vSddwZry4JnmltHVk= github.com/google/go-tpm v0.9.0/go.mod h1:FkNVkc6C+IsvDI9Jw1OveJmxGZUUaKxtrpOS47QWKfU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -782,6 +784,10 @@ github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6f github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/motemen/go-loghttp v0.0.0-20231107055348-29ae44b293f4 h1:WLWwzjax2/L5NAQul9bdk1EAP0+YGnAzJBJ/LzL8Dgs= +github.com/motemen/go-loghttp v0.0.0-20231107055348-29ae44b293f4/go.mod h1:ykaRC7b5xKciHTUFZ60bbsOojQAkCmmehBNbBWeIz1Y= +github.com/motemen/go-nuts v0.0.0-20220604134737-2658d0104f31 h1:lQ+0Zt2gm+w5+9iaBWKdJXC/gMrWjHhNbw9ts/9rSZ4= +github.com/motemen/go-nuts v0.0.0-20220604134737-2658d0104f31/go.mod h1:vkBO+XDNzovo+YLBpUod2SFvuWLObXlERnfj99RP3rU= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -907,6 +913,7 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw= github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -1012,6 +1019,8 @@ github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJ github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/workos/workos-go/v3 v3.2.0 h1:U+PfTR/cHmJo4BbkGI7rISU7xU5DgUSAi/+/2oqa8V8= +github.com/workos/workos-go/v3 v3.2.0/go.mod h1:SUdYqICB2LG2G2UMMNI2EcBYX9OdpzgpNYlW6k0JML4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= @@ -1226,6 +1235,7 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go index 4eac8be4f7db..6fbe29c2cde4 100644 --- a/selfservice/strategy/oidc/provider_config.go +++ b/selfservice/strategy/oidc/provider_config.go @@ -39,6 +39,7 @@ type Configuration struct { // - dingtalk // - linkedin // - patreon + // - workos Provider string `json:"provider"` // Label represents an optional label which can be used in the UI generation. @@ -81,6 +82,9 @@ type Configuration struct { // is used to generate `client_secret` TeamId string `json:"apple_team_id"` + // WorkOSOrganizationId is the WorkOS Organization ID that's needed for the `workos` `provider` to work. + WorkOSOrganizationId string `json:"workos_organization_id"` + // PrivateKeyId is the private Apple key identifier. Keys can be generated via developer.apple.com. // This key should be generated with the `Sign In with Apple` option checked. // This is needed when `provider` is set to `apple` @@ -161,6 +165,7 @@ var supportedProviders = map[string]func(config *Configuration, reg Dependencies "linkedin_v2": NewProviderLinkedInV2, "patreon": NewProviderPatreon, "lark": NewProviderLark, + "workos": NewProviderWorkOS, "x": NewProviderX, } diff --git a/selfservice/strategy/oidc/provider_workos.go b/selfservice/strategy/oidc/provider_workos.go new file mode 100644 index 000000000000..7f7c84ec9455 --- /dev/null +++ b/selfservice/strategy/oidc/provider_workos.go @@ -0,0 +1,86 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc + +import ( + "context" + "net/url" + + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/workos/workos-go/v3/pkg/sso" + + _ "github.com/motemen/go-loghttp/global" + + "github.com/ory/herodot" +) + +type ProviderWorkOS struct { + config *Configuration + reg Dependencies + ssoClient *sso.Client +} + +func NewProviderWorkOS( + config *Configuration, + reg Dependencies, +) Provider { + return &ProviderWorkOS{ + config: config, + reg: reg, + ssoClient: &sso.Client{ + APIKey: config.ClientSecret, + ClientID: config.ClientID, + }, + } +} + +func (g *ProviderWorkOS) Config() *Configuration { + return g.config +} + +func (g *ProviderWorkOS) oauth2(ctx context.Context) *oauth2.Config { + endpoint := oauth2.Endpoint{ + AuthURL: "https://api.workos.com/sso/authorize?organization=" + g.config.WorkOSOrganizationId, + TokenURL: "https://api.workos.com/sso/token", + } + + return &oauth2.Config{ + ClientID: g.config.ClientID, + ClientSecret: g.config.ClientSecret, + Endpoint: endpoint, + RedirectURL: g.config.Redir(g.reg.Config().OIDCRedirectURIBase(ctx)), + } +} + +func (g *ProviderWorkOS) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return g.oauth2(ctx), nil +} + +func (g *ProviderWorkOS) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{} +} + +func (g *ProviderWorkOS) Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error) { + profile, err := g.ssoClient.GetProfile( + ctx, + sso.GetProfileOpts{ + AccessToken: exchange.AccessToken, + }, + ) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + + claims := &Claims{ + Subject: profile.ID, + GivenName: profile.FirstName, + FamilyName: profile.LastName, + Email: profile.Email, + Issuer: "https://api.workos.com/sso/authorize?organization=" + g.config.WorkOSOrganizationId, + } + + return claims, nil +} diff --git a/selfservice/strategy/oidc/provider_workos_test.go b/selfservice/strategy/oidc/provider_workos_test.go new file mode 100644 index 000000000000..f1ac1d763ef2 --- /dev/null +++ b/selfservice/strategy/oidc/provider_workos_test.go @@ -0,0 +1,35 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/strategy/oidc" +) + +func TestProviderWorkOS(t *testing.T) { + _, reg := internal.NewVeryFastRegistryWithoutDB(t) + + p := oidc.NewProviderWorkOS(&oidc.Configuration{ + Provider: "workos", + ID: "demo_organization", + WorkOSOrganizationId: "demo_organization_id", + ClientID: "client", + ClientSecret: "secret", + Mapper: "file://./stub/hydra.schema.json", + RequestedClaims: nil, + Scope: []string{}, + }, reg) + + c, err := p.(oidc.OAuth2Provider).OAuth2(context.Background()) + require.NoError(t, err) + assert.Equal(t, "https://api.workos.com/sso/token", c.Endpoint.TokenURL) + assert.Equal(t, "https://api.workos.com/sso/authorize?organization=demo_organization_id", c.Endpoint.AuthURL) +}