Skip to content

Commit

Permalink
feat: add twitter SSO (#3778)
Browse files Browse the repository at this point in the history
  • Loading branch information
aeneasr committed Mar 1, 2024
1 parent 9710549 commit 930fb19
Show file tree
Hide file tree
Showing 22 changed files with 387 additions and 121 deletions.
3 changes: 2 additions & 1 deletion embedx/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,8 @@
"dingtalk",
"patreon",
"linkedin",
"lark"
"lark",
"x"
],
"examples": ["google"]
},
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ require (

require (
github.com/coreos/go-oidc/v3 v3.9.0
github.com/dghubble/oauth1 v0.7.2
github.com/lestrrat-go/jwx/v2 v2.0.19
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ github.com/davidrjonas/semver-cli v0.0.0-20190116233701-ee19a9a0dda6/go.mod h1:+
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/dghubble/oauth1 v0.7.2 h1:pwcinOZy8z6XkNxvPmUDY52M7RDPxt0Xw1zgZ6Cl5JA=
github.com/dghubble/oauth1 v0.7.2/go.mod h1:9erQdIhqhOHG/7K9s/tgh9Ks/AfoyrO5mW/43Lu2+kE=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
Expand Down
44 changes: 40 additions & 4 deletions identity/credentials_oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,36 @@ type CredentialsOIDCProvider struct {
Organization string `json:"organization,omitempty"`
}

// swagger:ignore
type CredentialsOIDCEncryptedTokens struct {
RefreshToken string `json:"refresh_token,omitempty"`
IDToken string `json:"id_token,omitempty"`
AccessToken string `json:"access_token,omitempty"`
}

func (c *CredentialsOIDCEncryptedTokens) GetRefreshToken() string {
if c == nil {
return ""
}
return c.RefreshToken
}

func (c *CredentialsOIDCEncryptedTokens) GetAccessToken() string {
if c == nil {
return ""
}
return c.AccessToken
}

func (c *CredentialsOIDCEncryptedTokens) GetIDToken() string {
if c == nil {
return ""
}
return c.IDToken
}

// NewCredentialsOIDC creates a new OIDC credential.
func NewCredentialsOIDC(idToken, accessToken, refreshToken, provider, subject, organization string) (*Credentials, error) {
func NewCredentialsOIDC(tokens *CredentialsOIDCEncryptedTokens, provider, subject, organization string) (*Credentials, error) {
if provider == "" {
return nil, errors.New("received empty provider in oidc credentials")
}
Expand All @@ -48,9 +76,9 @@ func NewCredentialsOIDC(idToken, accessToken, refreshToken, provider, subject, o
{
Subject: subject,
Provider: provider,
InitialIDToken: idToken,
InitialAccessToken: accessToken,
InitialRefreshToken: refreshToken,
InitialIDToken: tokens.GetIDToken(),
InitialAccessToken: tokens.GetAccessToken(),
InitialRefreshToken: tokens.GetRefreshToken(),
Organization: organization,
}},
}); err != nil {
Expand All @@ -65,6 +93,14 @@ func NewCredentialsOIDC(idToken, accessToken, refreshToken, provider, subject, o
}, nil
}

func (c *CredentialsOIDCProvider) GetTokens() *CredentialsOIDCEncryptedTokens {
return &CredentialsOIDCEncryptedTokens{
RefreshToken: c.InitialRefreshToken,
IDToken: c.InitialIDToken,
AccessToken: c.InitialAccessToken,
}
}

func OIDCUniqueID(provider, subject string) string {
return fmt.Sprintf("%s:%s", provider, subject)
}
Expand Down
6 changes: 3 additions & 3 deletions identity/credentials_oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import (
)

func TestNewCredentialsOIDC(t *testing.T) {
_, err := NewCredentialsOIDC("", "", "", "", "not-empty", "")
_, err := NewCredentialsOIDC(new(CredentialsOIDCEncryptedTokens), "", "not-empty", "")
require.Error(t, err)
_, err = NewCredentialsOIDC("", "", "", "not-empty", "", "")
_, err = NewCredentialsOIDC(new(CredentialsOIDCEncryptedTokens), "not-empty", "", "")
require.Error(t, err)
_, err = NewCredentialsOIDC("", "", "", "not-empty", "not-empty", "")
_, err = NewCredentialsOIDC(new(CredentialsOIDCEncryptedTokens), "not-empty", "not-empty", "")
require.NoError(t, err)
}
1 change: 1 addition & 0 deletions internal/client-go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Expand Down
4 changes: 1 addition & 3 deletions selfservice/flow/login/hook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,7 @@ func TestLoginExecutor(t *testing.T) {
require.NoError(t, reg.Persister().CreateIdentity(context.Background(), useIdentity))

credsOIDC, err := identity.NewCredentialsOIDC(
"id-token",
"access-token",
"refresh-token",
&identity.CredentialsOIDCEncryptedTokens{IDToken: "id-token", AccessToken: "access-token", RefreshToken: "refresh-token"},
"my-provider",
email,
"",
Expand Down
22 changes: 18 additions & 4 deletions selfservice/strategy/oidc/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ package oidc

import (
"context"
"net/http"
"net/url"

"github.com/dghubble/oauth1"
"github.com/pkg/errors"

"github.com/ory/herodot"
Expand All @@ -18,12 +20,24 @@ import (

type Provider interface {
Config() *Configuration
}

type OAuth2Provider interface {
Provider
AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption
OAuth2(ctx context.Context) (*oauth2.Config, error)
Claims(ctx context.Context, exchange *oauth2.Token, query url.Values) (*Claims, error)
AuthCodeURLOptions(r ider) []oauth2.AuthCodeOption
}

type TokenExchanger interface {
type OAuth1Provider interface {
Provider
OAuth1(ctx context.Context) *oauth1.Config
AuthURL(ctx context.Context, state string) (string, error)
Claims(ctx context.Context, token *oauth1.Token) (*Claims, error)
ExchangeToken(ctx context.Context, req *http.Request) (*oauth1.Token, error)
}

type OAuth2TokenExchanger interface {
Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
}

Expand Down Expand Up @@ -87,11 +101,11 @@ func (c *Claims) Validate() error {
// - `hd` (string): The `hd` parameter limits the login/registration process to a Google Organization, e.g. `mycollege.edu`.
// - `prompt` (string): The `prompt` specifies whether the Authorization Server prompts the End-User for reauthentication and consent, e.g. `select_account`.
// - `auth_type` (string): The `auth_type` parameter specifies the requested authentication features (as a comma-separated list), e.g. `reauthenticate`.
func UpstreamParameters(provider Provider, upstreamParameters map[string]string) []oauth2.AuthCodeOption {
func UpstreamParameters(upstreamParameters map[string]string) []oauth2.AuthCodeOption {
// validation of upstream parameters are already handled in the `oidc/.schema/link.schema.json` and `oidc/.schema/settings.schema.json` file.
// `upstreamParameters` will always only contain allowed parameters based on the configuration.

// we double check the parameters here to prevent any potential security issues.
// we double-check the parameters here to prevent any potential security issues.
allowedParameters := map[string]struct{}{
"login_hint": {},
"hd": {},
Expand Down
1 change: 1 addition & 0 deletions selfservice/strategy/oidc/provider_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ var supportedProviders = map[string]func(config *Configuration, reg Dependencies
"linkedin": NewProviderLinkedIn,
"patreon": NewProviderPatreon,
"lark": NewProviderLark,
"x": NewProviderX,
}

func (c ConfigurationCollection) Provider(id string, reg Dependencies) (Provider, error) {
Expand Down
2 changes: 1 addition & 1 deletion selfservice/strategy/oidc/provider_dingtalk.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (g *ProviderDingTalk) OAuth2(ctx context.Context) (*oauth2.Config, error) {
return g.oauth2(ctx), nil
}

func (g *ProviderDingTalk) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
func (g *ProviderDingTalk) ExchangeOAuth2Token(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
conf, err := g.OAuth2(ctx)
if err != nil {
return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("%s", err))
Expand Down
4 changes: 2 additions & 2 deletions selfservice/strategy/oidc/provider_generic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ func makeAuthCodeURL(t *testing.T, r *login.Flow, reg *driver.RegistryDefault) s
Mapper: "file://./stub/hydra.schema.json",
RequestedClaims: makeOIDCClaims(),
}, reg)
c, err := p.OAuth2(context.Background())
c, err := p.(oidc.OAuth2Provider).OAuth2(context.Background())
require.NoError(t, err)
return c.AuthCodeURL("state", p.AuthCodeURLOptions(r)...)
return c.AuthCodeURL("state", p.(oidc.OAuth2Provider).AuthCodeURLOptions(r)...)
}

func TestProviderGenericOIDC_AddAuthCodeURLOptions(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions selfservice/strategy/oidc/provider_google_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestProviderGoogle_Scope(t *testing.T) {
Scope: []string{"email", "profile", "offline_access"},
}, reg)

c, _ := p.OAuth2(context.Background())
c, _ := p.(oidc.OAuth2Provider).OAuth2(context.Background())
assert.NotContains(t, c.Scopes, "offline_access")
}

Expand All @@ -55,7 +55,7 @@ func TestProviderGoogle_AccessType(t *testing.T) {
ID: x.NewUUID(),
}

options := p.AuthCodeURLOptions(r)
options := p.(oidc.OAuth2Provider).AuthCodeURLOptions(r)
assert.Contains(t, options, oauth2.AccessTypeOffline)
}

Expand Down
4 changes: 2 additions & 2 deletions selfservice/strategy/oidc/provider_linkedin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func TestProviderLinkedin_Claims(t *testing.T) {
linkedin := oidc.NewProviderLinkedIn(c, reg)

const fakeLinkedinIDToken = "id_token_mock_"
actual, err := linkedin.Claims(
actual, err := linkedin.(oidc.OAuth2Provider).Claims(
context.Background(),
(&oauth2.Token{AccessToken: "foo", Expiry: time.Now().Add(time.Hour)}).WithExtra(map[string]interface{}{"id_token": fakeLinkedinIDToken}),
url.Values{},
Expand Down Expand Up @@ -191,7 +191,7 @@ func TestProviderLinkedin_No_Picture(t *testing.T) {
linkedin := oidc.NewProviderLinkedIn(c, reg)

const fakeLinkedinIDToken = "id_token_mock_"
actual, err := linkedin.Claims(
actual, err := linkedin.(oidc.OAuth2Provider).Claims(
context.Background(),
(&oauth2.Token{AccessToken: "foo", Expiry: time.Now().Add(time.Hour)}).WithExtra(map[string]interface{}{"id_token": fakeLinkedinIDToken}),
url.Values{},
Expand Down
3 changes: 2 additions & 1 deletion selfservice/strategy/oidc/provider_private_net_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,11 @@ func TestProviderPrivateIP(t *testing.T) {
// VK uses a fixed token URL and does not use the issuer.
// Yandex uses a fixed token URL and does not use the issuer.
// NetID uses a fixed token URL and does not use the issuer.
// X uses a fixed token URL and userinfoRL and does not use the issuer value.
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
p := tc.p(tc.c)
_, err := p.Claims(context.Background(), (&oauth2.Token{RefreshToken: "foo", Expiry: time.Now().Add(-time.Hour)}).WithExtra(map[string]interface{}{
_, err := p.(oidc.OAuth2Provider).Claims(context.Background(), (&oauth2.Token{RefreshToken: "foo", Expiry: time.Now().Add(-time.Hour)}).WithExtra(map[string]interface{}{
"id_token": tc.id,
}), url.Values{})
require.Error(t, err)
Expand Down
4 changes: 2 additions & 2 deletions selfservice/strategy/oidc/provider_userinfo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) {
return resp, err
})

_, err := tc.provider.Claims(ctx, token, url.Values{})
_, err := tc.provider.(oidc.OAuth2Provider).Claims(ctx, token, url.Values{})
var he *herodot.DefaultError
require.ErrorAs(t, err, &he)
assert.Equal(t, "OpenID Connect provider returned a 455 status code but 200 is expected.", he.Reason())
Expand All @@ -359,7 +359,7 @@ func TestProviderClaimsRespectsErrorCodes(t *testing.T) {

httpmock.RegisterResponder("GET", tc.userInfoEndpoint, tc.userInfoHandler)

claims, err := tc.provider.Claims(ctx, token, url.Values{})
claims, err := tc.provider.(oidc.OAuth2Provider).Claims(ctx, token, url.Values{})
require.NoError(t, err)
if tc.expectedClaims == nil {
assert.Equal(t, expectedClaims, claims)
Expand Down
Loading

0 comments on commit 930fb19

Please sign in to comment.