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: adds discord oidc provider #767

Merged
merged 7 commits into from
Oct 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions .schema/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,15 @@
},
"provider": {
"title": "Provider",
"description": "Can be one of github, gitlab, generic, google, microsoft.",
"description": "Can be one of github, gitlab, generic, google, microsoft, discord.",
"type": "string",
"enum": [
"github",
"gitlab",
"generic",
"google",
"microsoft"
"microsoft",
"discord"
],
"examples": [
"google"
Expand Down
91 changes: 91 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 @@ -35,6 +35,97 @@ selfservice:

:::

## Discord

To set up "Sign in with Discord" you must create a [Discord OAuth2 Application](https://discord.com/developers/docs/topics/oauth2).

Set the "Redirect URI" to:

```
http://127.0.0.1:4455/.ory/kratos/public/self-service/browser/flows/strategies/oidc/callback/discord
```

The pattern of this URL is:

```
http(s)://<domain-of-ory-kratos>:<public-port>/self-service/browser/flows/strategies/oidc/callback/<provider-id>
```

The provider ID must point to the provider's ID set in the ORY Kratos
configuration file (explained in further detail at
[OpenID Connect and OAuth2 Credentials](../concepts/credentials/openid-connect-oidc-oauth2.mdx)).

:::note

Discord does not implement OpenID Connect. Therefore, ORY Kratos makes a request to
[Discord's User API](https://discord.com/developers/docs/resources/user#get-current-user)
and adds the following claims to `std.extVar('claims')`:

With scope `identify`:

```text
iss # always https://discord.com/api/v6/oauth2/
sub # numeric discord user id
name # username + # + discriminator
nickname # username
preferred_username # username
picture # avatar url
locale # user locale
```

With Scope `email`:

```text
iss # always https://discord.com/api/v6/oauth2/
email # email
email_verified # whether email is verified
```

:::

```json title="contrib/quickstart/kratos/email-password/oidc.discord.jsonnet"
local claims = {
email_verified: false
} + std.extVar('claims');

{
identity: {
traits: {
// Allowing unverified email addresses enables account
// enumeration attacks, especially if the value is used for
// e.g. verification or as a password login identifier.
//
// Therefore we only return the email if it (a) exists and (b) is marked verified
// by Discord.
[if "email" in claims && claims.email_verified then "email" else null]: claims.email,
},
},
}
```

Now, enable the Discord 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:
strategies:
oidc:
enabled: true
config:
providers:
- id: discord # this is `<provider-id>` in the Authorization callback URL. DO NOT CHANGE IT ONCE SET!
provider: discord
client_id: .... # Replace this with the OAuth2 Client ID provided by Discord
client_secret: .... # Replace this with the OAuth2 Client Secret provided by Discord
mapper_url: file:///etc/config/kratos/oidc.discord.jsonnet
scope:
- identify
- email
```

Discord is now an option to log in via Kratos.

## GitHub

<iframe
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0
github.com/armon/go-metrics v0.3.3 // indirect
github.com/bwmarrin/discordgo v0.22.0
github.com/bxcodec/faker/v3 v3.3.1
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/coreos/go-oidc v2.2.1+incompatible
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bwmarrin/discordgo v0.22.0 h1:uBxY1HmlVCsW1IuaPjpCGT6A2DBwRn0nvOguQIxDdFM=
github.com/bwmarrin/discordgo v0.22.0/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
github.com/bxcodec/faker/v3 v3.3.1 h1:G7uldFk+iO/ES7W4v7JlI/WU9FQ6op9VJ15YZlDEhGQ=
github.com/bxcodec/faker/v3 v3.3.1/go.mod h1:gF31YgnMSMKgkvl+fyEo1xuSMbEuieyqfeslGYFjneM=
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
Expand Down Expand Up @@ -696,6 +698,7 @@ github.com/gorilla/sessions v1.1.2/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotestyourself/gotestyourself v1.3.0 h1:9X3T0HDKAY/58/sEPpTkmyOg4wbb1ab9tZfV44mTSeE=
github.com/gotestyourself/gotestyourself v1.3.0/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
Expand Down Expand Up @@ -1360,6 +1363,7 @@ golang.org/x/crypto v0.0.0-20181024171144-74cb1d3d52f4/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20181025113841-85e1b3f9139a/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
Expand Down
3 changes: 3 additions & 0 deletions selfservice/strategy/oidc/provider_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type Configuration struct {
// - github
// - gitlab
// - microsoft
// - discord
Provider string `json:"provider"`

// ClientID is the application's Client ID.
Expand Down Expand Up @@ -96,6 +97,8 @@ func (c ConfigurationCollection) Provider(id string, public *url.URL) (Provider,
return NewProviderGitLab(&p, public), nil
case addProviderName("microsoft"):
return NewProviderMicrosoft(&p, public), nil
case addProviderName("discord"):
return NewProviderDiscord(&p, public), nil
}
return nil, errors.Errorf("provider type %s is not supported, supported are: %v", p.Provider, providerNames)
}
Expand Down
94 changes: 94 additions & 0 deletions selfservice/strategy/oidc/provider_discord.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package oidc

import (
"context"
"fmt"
"net/url"

"github.com/bwmarrin/discordgo"
"github.com/pkg/errors"
"golang.org/x/oauth2"

"github.com/ory/herodot"
"github.com/ory/x/stringslice"
"github.com/ory/x/stringsx"
)

type ProviderDiscord struct {
config *Configuration
public *url.URL
}

func NewProviderDiscord(
config *Configuration,
public *url.URL,
) *ProviderDiscord {
return &ProviderDiscord{
config: config,
public: public,
}
}

func (d *ProviderDiscord) Config() *Configuration {
return d.config
}

func (d *ProviderDiscord) oauth2() *oauth2.Config {
return &oauth2.Config{
ClientID: d.config.ClientID,
ClientSecret: d.config.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: discordgo.EndpointOauth2 + "authorize",
TokenURL: discordgo.EndpointOauth2 + "token",
},
RedirectURL: d.config.Redir(d.public),
Scopes: d.config.Scope,
}
}

func (d *ProviderDiscord) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return d.oauth2(), nil
}

func (d *ProviderDiscord) AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption {
if isForced(r) {
return []oauth2.AuthCodeOption{
oauth2.SetAuthURLParam("prompt", "consent"),
}
}
return []oauth2.AuthCodeOption{}
}

func (d *ProviderDiscord) Claims(ctx context.Context, exchange *oauth2.Token) (*Claims, error) {
grantedScopes := stringsx.Splitx(fmt.Sprintf("%s", exchange.Extra("scope")), " ")
for _, check := range d.Config().Scope {
if !stringslice.Has(grantedScopes, check) {
return nil, errors.WithStack(ErrScopeMissing)
}
}

dg, err := discordgo.New(fmt.Sprintf("Bearer %s", exchange.AccessToken))
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
}

// TODO: upgrade github.com/bwmarrin/discordgo once it supports api v8: https://github.com/bwmarrin/discordgo/issues/822
user, err := dg.User("@me")
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
}

claims := &Claims{
Issuer: discordgo.EndpointOauth2,
Subject: user.ID,
Name: fmt.Sprintf("%s#%s", user.Username, user.Discriminator),
Nickname: user.Username,
PreferredUsername: user.Username,
Picture: user.AvatarURL(""),
Email: user.Email,
EmailVerified: user.Verified,
Locale: user.Locale,
}

return claims, nil
}