Skip to content

Commit

Permalink
feat(oidc): add spotify provider (#2024)
Browse files Browse the repository at this point in the history
Co-authored-by: Keith Mitchell <[email protected]>
  • Loading branch information
aeneasr and xkisu authored Dec 8, 2021
1 parent 2637470 commit 0064e35
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 4 deletions.
64 changes: 64 additions & 0 deletions docs/docs/guides/sign-in-with-github-google-facebook-linkedin.mdx
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 @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
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
95 changes: 95 additions & 0 deletions selfservice/strategy/oidc/provider_spotify.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 0064e35

Please sign in to comment.