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(oidc): add spotify provider #2024

Merged
merged 6 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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,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)://<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
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
)
6 changes: 5 additions & 1 deletion 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 @@ -2189,8 +2192,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=
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/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) {
fmt.Println("SCOPE MISSING", check, fmt.Sprintf("%#v", exchange.Extra("scope")))
aeneasr marked this conversation as resolved.
Show resolved Hide resolved
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,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there! I'm looking to make a contribution here in order to get Country from the Spotify API. Would it be simple as adding Country: user.country into the Claims struct here?

My apologies - I'm not familiar with the providers or Go. But I'd love to make my first very simple contribution to Kratos.

Should this change be accepted, what is the average turn-around on a release for a non-breaking change like this making it to Ory Cloud? This is a requirement for a system I am building.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be simple as adding Country: user.country into the Claims struct here?

Not sure, but I suggest you try it with a local Kratos instance and your patch. Make sure to check the spotify docs on the exact key and scopes needed.

what is the average turn-around on a release for a non-breaking change like this making it to Ory Cloud?

Days, but we can prioritize this when requested.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The country of the user, as set in the user's account profile. An ISO 3166-1 alpha-2 country code. This field is only available when the current user has granted access to the user-read-private scope.

from https://developer.spotify.com/documentation/web-api/reference/#/operations/get-current-users-profile

}

return claims, nil
}