From 6a4f52911289e365b8278b0eaf50b0ba16bc3899 Mon Sep 17 00:00:00 2001 From: Keith Mitchell Date: Wed, 24 Nov 2021 15:15:50 +0000 Subject: [PATCH 1/5] feat(oidc): add spotify provider --- embedx/config.schema.json | 5 +- go.mod | 3 +- go.sum | 4 + selfservice/strategy/oidc/provider_config.go | 2 + selfservice/strategy/oidc/provider_spotify.go | 96 +++++++++++++++++++ 5 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 selfservice/strategy/oidc/provider_spotify.go diff --git a/embedx/config.schema.json b/embedx/config.schema.json index f55a4c380e57..c75eceef15e7 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -310,7 +310,7 @@ }, "provider": { "title": "Provider", - "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, slack, facebook, auth0, vk, yandex.", + "description": "Can be one of github, github-app, gitlab, generic, google, microsoft, discord, slack, facebook, auth0, vk, yandex, spotify.", "type": "string", "enum": [ "github", @@ -325,7 +325,8 @@ "auth0", "vk", "yandex", - "apple" + "apple", + "spotify" ], "examples": [ "google" diff --git a/go.mod b/go.mod index 8d3bcd966107..67ba58125481 100644 --- a/go.mod +++ b/go.mod @@ -90,9 +90,10 @@ require ( github.com/tidwall/gjson v1.9.4 github.com/tidwall/sjson v1.2.2 github.com/urfave/negroni v1.0.0 + github.com/zmb3/spotify/v2 v2.0.0 // indirect golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 golang.org/x/net v0.0.0-20211020060615-d418f374d309 - golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c + golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/tools v0.1.7 ) diff --git a/go.sum b/go.sum index 483523e8391f..6a07e656a0dd 100644 --- a/go.sum +++ b/go.sum @@ -1933,6 +1933,8 @@ github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPS github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= +github.com/zmb3/spotify/v2 v2.0.0 h1:NHW9btztNZTrJ0+3yMNyfY5qcu1ck9s36wwzc7zrCic= +github.com/zmb3/spotify/v2 v2.0.0/go.mod h1:+LVh9CafHu7SedyqYmEf12Rd01dIVlEL845yNhksW0E= go.elastic.co/apm v1.8.0/go.mod h1:tCw6CkOJgkWnzEthFN9HUP1uL3Gjc/Ur6m7gRPLaoH0= go.elastic.co/apm v1.13.0/go.mod h1:dylGv2HKR0tiCV+wliJz1KHtDyuD8SPe69oV7VyK6WY= go.elastic.co/apm v1.14.0 h1:9yilcTbWpqhfyunUj6/SDpZbR4FOVB50xQgODe0TW/0= @@ -2173,6 +2175,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211020060615-d418f374d309 h1:A0lJIi+hcTR6aajJH4YqKWwohY4aW9RO7oRMcdv+HKI= golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -2191,6 +2194,7 @@ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI= 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/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/selfservice/strategy/oidc/provider_config.go b/selfservice/strategy/oidc/provider_config.go index 6723081e145c..f53e3062b782 100644 --- a/selfservice/strategy/oidc/provider_config.go +++ b/selfservice/strategy/oidc/provider_config.go @@ -136,6 +136,8 @@ func (c ConfigurationCollection) Provider(id string, public *url.URL) (Provider, return NewProviderYandex(&p, public), nil case addProviderName("apple"): return NewProviderApple(&p, public), nil + case addProviderName("spotify"): + return NewProviderSpotify(&p, public), nil } return nil, errors.Errorf("provider type %s is not supported, supported are: %v", p.Provider, providerNames) } diff --git a/selfservice/strategy/oidc/provider_spotify.go b/selfservice/strategy/oidc/provider_spotify.go new file mode 100644 index 000000000000..2548376e93cc --- /dev/null +++ b/selfservice/strategy/oidc/provider_spotify.go @@ -0,0 +1,96 @@ +package oidc + +import ( + "context" + "fmt" + "net/url" + + "golang.org/x/oauth2/spotify" + + "github.com/ory/x/stringslice" + "github.com/ory/x/stringsx" + "github.com/pkg/errors" + "golang.org/x/oauth2" + + spotifyapi "github.com/zmb3/spotify/v2" + spotifyauth "github.com/zmb3/spotify/v2/auth" + + "github.com/ory/herodot" +) + +type ProviderSpotify struct { + config *Configuration + public *url.URL +} + +func NewProviderSpotify( + config *Configuration, + public *url.URL, +) *ProviderSpotify { + return &ProviderSpotify{ + config: config, + public: public, + } +} + +func (g *ProviderSpotify) Config() *Configuration { + return g.config +} + +func (g *ProviderSpotify) oauth2() *oauth2.Config { + return &oauth2.Config{ + ClientID: g.config.ClientID, + ClientSecret: g.config.ClientSecret, + Endpoint: spotify.Endpoint, + Scopes: g.config.Scope, + RedirectURL: g.config.Redir(g.public), + } +} + +func (g *ProviderSpotify) OAuth2(ctx context.Context) (*oauth2.Config, error) { + return g.oauth2(), nil +} + +func (g *ProviderSpotify) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption { + return []oauth2.AuthCodeOption{} +} + +func (g *ProviderSpotify) Claims(ctx context.Context, exchange *oauth2.Token) (*Claims, error) { + grantedScopes := stringsx.Splitx(fmt.Sprintf("%s", exchange.Extra("scope")), " ") + for _, check := range g.Config().Scope { + if !stringslice.Has(grantedScopes, check) { + fmt.Println("SCOPE MISSING", check, fmt.Sprintf("%#v", exchange.Extra("scope"))) + return nil, errors.WithStack(ErrScopeMissing) + } + } + + auth := spotifyauth.New( + spotifyauth.WithRedirectURL(g.config.Redir(g.public)), + spotifyauth.WithScopes(spotifyauth.ScopeUserReadPrivate)) + + client := spotifyapi.New(auth.Client(ctx, exchange)) + + user, err := client.CurrentUser(ctx) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err)) + } + + var userPicture string + if len(user.Images) > 0 { + userPicture = user.Images[0].URL + } + + claims := &Claims{ + Subject: user.ID, + Issuer: spotify.Endpoint.TokenURL, + Name: user.DisplayName, + Nickname: user.DisplayName, + Email: user.Email, + Picture: userPicture, + Profile: user.ExternalURLs["spotify"], + Birthdate: user.Birthdate, + } + + return claims, nil +} + From 491ccb7f4d78f5502f1a8f7023c9c30fbb8bad03 Mon Sep 17 00:00:00 2001 From: Keith Mitchell Date: Wed, 24 Nov 2021 15:27:07 +0000 Subject: [PATCH 2/5] docs: add spotify instructions to social sign in docs --- ...n-with-github-google-facebook-linkedin.mdx | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/docs/docs/guides/sign-in-with-github-google-facebook-linkedin.mdx b/docs/docs/guides/sign-in-with-github-google-facebook-linkedin.mdx index c36ecd4d7d40..667e905f9c8b 100644 --- a/docs/docs/guides/sign-in-with-github-google-facebook-linkedin.mdx +++ b/docs/docs/guides/sign-in-with-github-google-facebook-linkedin.mdx @@ -1078,6 +1078,68 @@ selfservice: Next, open the login endpoint of the SecureApp and you should see the Apple Login option! +## Spotify + +To set up "Sign in with Spotify" you must create an +[Spotify Application](https://developer.spotify.com/dashboard/applications). + +Set the "Redirect URI" to: + +``` +https://playground.projects.oryapis.com/api/kratos/public/self-service/methods/oidc/callback/spotify +``` + +The pattern of this URL is: + +``` +http(s)://:/self-service/methods/oidc/callback/ +``` + +:::note + +While Spotify [provides an OIDC discovery URL](https://accounts.spotify.com/.well-known/openid-configuration), +Spotify does not actually support the `openid` claim and only returns an access token. Therefore, Ory Kratos +makes a request to [Spotify's /me API](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-current-users-profile) +and adds the user info to `std.extVar('claims')`. + +::: + +```json title="contrib/quickstart/kratos/email-password/oidc.spotify.jsonnet" +# claims contains all the data sent by the upstream. +local claims = std.extVar('claims'); + +{ + identity: { + traits: { + email: claims.email + }, + }, +} +``` + +Now, enable the Spotify provider in the Ory Kratos config located at +`/contrib/quickstart/kratos/email-password/kratos.yml`. + +```yaml title="contrib/quickstart/kratos/email-password/kratos.yml" +# $ kratos -c path/to/my/kratos/config.yml serve +selfservice: + methods: + oidc: + enabled: true + config: + providers: + - id: spotify # this is `` in the Authorization callback URL. DO NOT CHANGE IT ONCE SET! + provider: spotify + client_id: .... # Replace this with the OAuth2 Client ID provided by Spotify + client_secret: .... # Replace this with the OAuth2 Client Secret provided by Spotify + mapper_url: file:///etc/config/kratos/oidc.spotify.jsonnet + scope: + - user-read-email + - user-read-private +``` + +Spotify is now an option to log in via Kratos. + ## LinkedIn Connecting with other Social Sign In providers will be very similar to the From 992f8093e0d1981e4c2721fa67a8fbe7b3248d59 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Mon, 6 Dec 2021 18:03:35 +0100 Subject: [PATCH 3/5] chore: code review --- .../sign-in-with-github-google-facebook-linkedin.mdx | 8 +++++--- selfservice/strategy/oidc/provider_spotify.go | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/docs/guides/sign-in-with-github-google-facebook-linkedin.mdx b/docs/docs/guides/sign-in-with-github-google-facebook-linkedin.mdx index 667e905f9c8b..ea1f5f329131 100644 --- a/docs/docs/guides/sign-in-with-github-google-facebook-linkedin.mdx +++ b/docs/docs/guides/sign-in-with-github-google-facebook-linkedin.mdx @@ -1097,9 +1097,11 @@ http(s)://:/self-service/methods/oidc/callbac :::note -While Spotify [provides an OIDC discovery URL](https://accounts.spotify.com/.well-known/openid-configuration), -Spotify does not actually support the `openid` claim and only returns an access token. Therefore, Ory Kratos -makes a request to [Spotify's /me API](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-current-users-profile) +While Spotify +[provides an OIDC discovery URL](https://accounts.spotify.com/.well-known/openid-configuration), +Spotify does not actually support the `openid` claim and only returns an access +token. Therefore, Ory Kratos makes a request to +[Spotify's /me API](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-current-users-profile) and adds the user info to `std.extVar('claims')`. ::: diff --git a/selfservice/strategy/oidc/provider_spotify.go b/selfservice/strategy/oidc/provider_spotify.go index 2548376e93cc..b7611c33ae42 100644 --- a/selfservice/strategy/oidc/provider_spotify.go +++ b/selfservice/strategy/oidc/provider_spotify.go @@ -7,11 +7,12 @@ import ( "golang.org/x/oauth2/spotify" - "github.com/ory/x/stringslice" - "github.com/ory/x/stringsx" "github.com/pkg/errors" "golang.org/x/oauth2" + "github.com/ory/x/stringslice" + "github.com/ory/x/stringsx" + spotifyapi "github.com/zmb3/spotify/v2" spotifyauth "github.com/zmb3/spotify/v2/auth" @@ -93,4 +94,3 @@ func (g *ProviderSpotify) Claims(ctx context.Context, exchange *oauth2.Token) (* return claims, nil } - From f371418f4190e18cd89311cbfd145c1f19eade61 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Mon, 6 Dec 2021 18:06:02 +0100 Subject: [PATCH 4/5] go mod --- go.mod | 2 +- go.sum | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 67ba58125481..eb972594308f 100644 --- a/go.mod +++ b/go.mod @@ -90,7 +90,7 @@ require ( github.com/tidwall/gjson v1.9.4 github.com/tidwall/sjson v1.2.2 github.com/urfave/negroni v1.0.0 - github.com/zmb3/spotify/v2 v2.0.0 // indirect + github.com/zmb3/spotify/v2 v2.0.0 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 golang.org/x/net v0.0.0-20211020060615-d418f374d309 golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 diff --git a/go.sum b/go.sum index 6a07e656a0dd..bcc143054695 100644 --- a/go.sum +++ b/go.sum @@ -2192,8 +2192,8 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c h1:pkQiBZBvdos9qq4wBAHqlzuZHEXo07pqV06ef90u1WI= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 h1:Ati8dO7+U7mxpkPSxBZQEvzHVUYB/MqCklCN8ig5w/o= golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= From 342fd2f555a881832e779e8997bdd0250cb06f7a Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Mon, 6 Dec 2021 19:43:34 +0100 Subject: [PATCH 5/5] Apply suggestions from code review --- selfservice/strategy/oidc/provider_spotify.go | 1 - 1 file changed, 1 deletion(-) diff --git a/selfservice/strategy/oidc/provider_spotify.go b/selfservice/strategy/oidc/provider_spotify.go index b7611c33ae42..13282ed110c5 100644 --- a/selfservice/strategy/oidc/provider_spotify.go +++ b/selfservice/strategy/oidc/provider_spotify.go @@ -60,7 +60,6 @@ func (g *ProviderSpotify) Claims(ctx context.Context, exchange *oauth2.Token) (* grantedScopes := stringsx.Splitx(fmt.Sprintf("%s", exchange.Extra("scope")), " ") for _, check := range g.Config().Scope { if !stringslice.Has(grantedScopes, check) { - fmt.Println("SCOPE MISSING", check, fmt.Sprintf("%#v", exchange.Extra("scope"))) return nil, errors.WithStack(ErrScopeMissing) } }