snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
173
selfservice/strategy/oidc/provider_x.go
Normal file
173
selfservice/strategy/oidc/provider_x.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright © 2024 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/ory/x/otelx"
|
||||
|
||||
"github.com/dghubble/oauth1"
|
||||
"github.com/dghubble/oauth1/twitter"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/ory/herodot"
|
||||
)
|
||||
|
||||
var _ OAuth1Provider = (*ProviderX)(nil)
|
||||
|
||||
const (
|
||||
xUserInfoBase = "https://api.twitter.com/1.1/account/verify_credentials.json"
|
||||
xUserInfoWithEmail = xUserInfoBase + "?include_email=true"
|
||||
)
|
||||
|
||||
type ProviderX struct {
|
||||
config *Configuration
|
||||
reg Dependencies
|
||||
}
|
||||
|
||||
func (p *ProviderX) Config() *Configuration {
|
||||
return p.config
|
||||
}
|
||||
|
||||
func NewProviderX(
|
||||
config *Configuration,
|
||||
reg Dependencies,
|
||||
) Provider {
|
||||
return &ProviderX{
|
||||
config: config,
|
||||
reg: reg,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProviderX) ExchangeToken(ctx context.Context, req *http.Request) (*oauth1.Token, error) {
|
||||
requestToken, verifier, err := oauth1.ParseAuthorizationCallback(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessToken, accessSecret, err := p.OAuth1(ctx).AccessToken(requestToken, "", verifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return oauth1.NewToken(accessToken, accessSecret), nil
|
||||
}
|
||||
|
||||
func (p *ProviderX) AuthURL(ctx context.Context, state string) (_ string, err error) {
|
||||
ctx, span := p.reg.Tracer(ctx).Tracer().Start(ctx, "selfservice.strategy.oidc.ProviderLinkedIn.fetch")
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
c := p.OAuth1(ctx)
|
||||
|
||||
// We need to cheat so that callback validates on return
|
||||
c.CallbackURL = c.CallbackURL + fmt.Sprintf("?state=%s&code=unused", state)
|
||||
|
||||
requestToken, _, err := c.RequestToken()
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return "", errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf(`Unable to sign in with X because the OAuth1 request token could not be initialized: %s`, err))
|
||||
}
|
||||
|
||||
authzURL, err := c.AuthorizationURL(requestToken)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
return "", errors.WithStack(herodot.ErrMisconfiguration().WithWrap(err).WithReasonf(`Unable to sign in with X because the OAuth1 authorization URL could not be parsed: %s`, err))
|
||||
}
|
||||
|
||||
return authzURL.String(), nil
|
||||
}
|
||||
|
||||
func (p *ProviderX) CheckError(ctx context.Context, r *http.Request) error {
|
||||
if r.URL.Query().Get("denied") == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.WithStack(herodot.ErrBadRequest().WithReasonf(`Unable to sign in with X because the user denied the request.`))
|
||||
}
|
||||
|
||||
func (p *ProviderX) OAuth1(ctx context.Context) *oauth1.Config {
|
||||
return &oauth1.Config{
|
||||
ConsumerKey: p.config.ClientID,
|
||||
ConsumerSecret: p.config.ClientSecret,
|
||||
Endpoint: twitter.AuthenticateEndpoint,
|
||||
CallbackURL: p.config.Redir(p.reg.Config().OIDCRedirectURIBase(ctx)),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProviderX) userInfoEndpoint() string {
|
||||
for _, scope := range p.config.Scope {
|
||||
if scope == "email" {
|
||||
return xUserInfoWithEmail
|
||||
}
|
||||
}
|
||||
|
||||
return xUserInfoBase
|
||||
}
|
||||
|
||||
func (p *ProviderX) Claims(ctx context.Context, token *oauth1.Token) (*Claims, error) {
|
||||
ctx = context.WithValue(ctx, oauth1.HTTPClient, p.reg.HTTPClient(ctx).HTTPClient)
|
||||
|
||||
c := p.OAuth1(ctx)
|
||||
client := c.Client(ctx, token)
|
||||
endpoint := p.userInfoEndpoint()
|
||||
|
||||
resp, err := client.Get(endpoint)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if err := logUpstreamError(p.reg.Logger(), resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &xUser{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(user); err != nil {
|
||||
return nil, errors.WithStack(herodot.ErrUpstreamError().WithWrap(err).WithReasonf("%s", err))
|
||||
}
|
||||
|
||||
website := ""
|
||||
if user.URL != nil {
|
||||
website = *user.URL
|
||||
}
|
||||
|
||||
return &Claims{
|
||||
Issuer: endpoint,
|
||||
Subject: user.IDStr,
|
||||
Name: user.Name,
|
||||
Picture: user.ProfileImageURLHTTPS,
|
||||
Email: user.Email,
|
||||
PreferredUsername: user.ScreenName,
|
||||
Website: website,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type xUser struct {
|
||||
ID int `json:"id"`
|
||||
IDStr string `json:"id_str"`
|
||||
Name string `json:"name"`
|
||||
ScreenName string `json:"screen_name"`
|
||||
Location string `json:"location"`
|
||||
Description string `json:"description"`
|
||||
URL *string `json:"url,omitempty"`
|
||||
Protected bool `json:"protected"`
|
||||
FollowersCount int `json:"followers_count"`
|
||||
FriendsCount int `json:"friends_count"`
|
||||
ListedCount int `json:"listed_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
FavouritesCount int `json:"favourites_count"`
|
||||
Verified bool `json:"verified"`
|
||||
StatusesCount int `json:"statuses_count"`
|
||||
DefaultProfile bool `json:"default_profile"`
|
||||
DefaultProfileImage bool `json:"default_profile_image"`
|
||||
ProfileImageURLHTTPS string `json:"profile_image_url_https"`
|
||||
WithheldInCountries []string `json:"withheld_in_countries"`
|
||||
Suspended bool `json:"suspended"`
|
||||
NeedsPhoneVerification bool `json:"needs_phone_verification"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
Reference in New Issue
Block a user