Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(selfservice/strategy/oidc): add spotify provider #1985

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)://<domain-of-ory-kratos>:<public-port>/self-service/methods/oidc/callback/<provider-id>
```

:::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
`<kratos-directory>/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 `<provider-id>` 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
Expand Down
5 changes: 3 additions & 2 deletions embedx/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -325,7 +325,8 @@
"auth0",
"vk",
"yandex",
"apple"
"apple",
"spotify"
],
"examples": [
"google"
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
Expand Down
2 changes: 2 additions & 0 deletions selfservice/strategy/oidc/provider_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
96 changes: 96 additions & 0 deletions selfservice/strategy/oidc/provider_spotify.go
Original file line number Diff line number Diff line change
@@ -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
}