Files
upstream-mirror c60cc7ca75
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
snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:19:12 +05:00

174 lines
5.0 KiB
Go

// 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"`
}