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 38856294f9be..cc5491ef1eca 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,70 @@ 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 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 ceacb212f6de..b4d5001dc9dc 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 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 8dcd89c306d0..d0e7cfe64ab3 100644 --- a/go.sum +++ b/go.sum @@ -1937,6 +1937,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= @@ -2177,6 +2179,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= @@ -2193,8 +2196,9 @@ 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= 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..13282ed110c5 --- /dev/null +++ b/selfservice/strategy/oidc/provider_spotify.go @@ -0,0 +1,95 @@ +package oidc + +import ( + "context" + "fmt" + "net/url" + + "golang.org/x/oauth2/spotify" + + "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" + + "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) { + 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 +}