diff --git a/app/app.go b/app/app.go index 77333c6d6..56b660328 100644 --- a/app/app.go +++ b/app/app.go @@ -132,6 +132,9 @@ func initializeOAuthProviders(cfg *Config) (map[string]oauth.Provider, error) { if cfg.FacebookOauthCredentials != nil { oauthProviders["facebook"] = *oauth.NewFacebookProvider(cfg.FacebookOauthCredentials) } + if cfg.TwitterOauthCredentials != nil { + oauthProviders["twitter"] = *oauth.NewTwitterProvider(cfg.TwitterOauthCredentials) + } if cfg.DiscordOauthCredentials != nil { oauthProviders["discord"] = *oauth.NewDiscordProvider(cfg.DiscordOauthCredentials) } diff --git a/app/config.go b/app/config.go index 50fe9de1d..f2ceb938e 100644 --- a/app/config.go +++ b/app/config.go @@ -74,6 +74,7 @@ type Config struct { GoogleOauthCredentials *oauth.Credentials GitHubOauthCredentials *oauth.Credentials FacebookOauthCredentials *oauth.Credentials + TwitterOauthCredentials *oauth.Credentials DiscordOauthCredentials *oauth.Credentials MicrosoftOauthCredentials *oauth.Credentials AppleOAuthCredentials *oauth.Credentials @@ -85,6 +86,7 @@ func (c *Config) OAuthEnabled() bool { return c.GoogleOauthCredentials != nil || c.GitHubOauthCredentials != nil || c.FacebookOauthCredentials != nil || + c.TwitterOauthCredentials != nil || c.DiscordOauthCredentials != nil || c.MicrosoftOauthCredentials != nil || c.AppleOAuthCredentials != nil @@ -626,6 +628,19 @@ var configurers = []configurer{ return nil }, + // TWITTER_OAUTH_CREDENTIALS is a credential pair in the format `id:secret`. When specified, + // AuthN will enable routes for Twitter OAuth signin. + func(c *Config) error { + if val, ok := os.LookupEnv("TWITTER_OAUTH_CREDENTIALS"); ok { + credentials, err := oauth.NewCredentials(val) + if err == nil { + c.TwitterOauthCredentials = credentials + } + return err + } + return nil + }, + // DISCORD_OAUTH_CREDENTIALS is a credential pair in the format `id:secret`. When specified, // AuthN will enable routes for Discord OAuth signin. func(c *Config) error { diff --git a/docs/api.md b/docs/api.md index a85884651..ae209f44f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -674,6 +674,7 @@ Visibility: Public * google * github * facebook | +* twitter | This is the return URL that must be registered with a provider when provisioning credentials. From here, a user will proceed to the `redirect_uri` specified at the [Begin OAuth](#begin-oauth) step. diff --git a/docs/config.md b/docs/config.md index 272cfcde9..d8e0ce676 100644 --- a/docs/config.md +++ b/docs/config.md @@ -4,7 +4,7 @@ * Databases: [`DATABASE_URL`](#database_url) • [`REDIS_URL`](#redis_url) • [`REDIS_IS_SENTINEL_MODE`](#redis_is_sentinel_mode) • [`REDIS_SENTINEL_MASTER`](#redis_sentinel_master) • [`REDIS_SENTINEL_NODES`](#redis_sentinel_nodes) • [`REDIS_SENTINEL_PASSWORD`](#redis_sentinel_password) * Sessions: [`ACCESS_TOKEN_TTL`](#access_token_ttl) • [`REFRESH_TOKEN_TTL`](#refresh_token_ttl)• [`REFRESH_TOKEN_EXPLICIT_EXPIRY`](#refresh_token_explicit_expiry) • [`SESSION_KEY_SALT`](#session_key_salt) • [`DB_ENCRYPTION_KEY_SALT`](#db_encryption_key_salt) • [`RSA_PRIVATE_KEY`](#rsa_private_key) • [`SAME_SITE`](#same_site) -* OAuth Clients: [`FACEBOOK_OAUTH_CREDENTIALS`](#facebook_oauth_credentials) • [`GITHUB_OAUTH_CREDENTIALS`](#github_oauth_credentials) • [`GOOGLE_OAUTH_CREDENTIALS`](#google_oauth_credentials) • [`DISCORD_OAUTH_CREDENTIALS`](#discord_oauth_credentials) • [`MICROSOFT_OAUTH_CREDENTIALS`](#microsoft_oauth_credentials) +* OAuth Clients: [`FACEBOOK_OAUTH_CREDENTIALS`](#facebook_oauth_credentials) • [`TWITTER_OAUTH_CREDENTIALS`](#twitter_oauth_credentials) • [`GITHUB_OAUTH_CREDENTIALS`](#github_oauth_credentials) • [`GOOGLE_OAUTH_CREDENTIALS`](#google_oauth_credentials) • [`DISCORD_OAUTH_CREDENTIALS`](#discord_oauth_credentials) • [`MICROSOFT_OAUTH_CREDENTIALS`](#microsoft_oauth_credentials) * Username Policy: [`USERNAME_IS_EMAIL`](#username_is_email) • [`EMAIL_USERNAME_DOMAINS`](#email_username_domains) * Password Policy: [`PASSWORD_POLICY_SCORE`](#password_policy_score) • [`PASSWORD_CHANGE_LOGOUT`](#password_change_logout) • [`BCRYPT_COST`](#bcrypt_cost) * Password Resets: [`APP_PASSWORD_RESET_URL`](#app_password_reset_url) • [`PASSWORD_RESET_TOKEN_TTL`](#password_reset_token_ttl) • [`APP_PASSWORD_CHANGED_URL`](#app_password_changed_url) @@ -286,6 +286,18 @@ The configured client secret is a private key used to sign the JWT. This should Create a Facebook app at https://developers.facebook.com and enable the Facebook Login product. In the Quickstart, enter [AuthN's OAuth Return](api.md#oauth-return) as the Site URL. Then switch over to Settings and find the App ID and Secret. Join those together with a `:` and provide them to AuthN as a single variable. +### `TWITTER_OAUTH_CREDENTIALS` + +| | | +|-----------|-----------------| +| Required? | No | +| Value | AppID:AppSecret | +| Default | nil | + +Log in and see your apps in the [X developer portal](https://developer.x.com/en/portal/projects-and-apps). Go to app settings and set up user authentication. Enter [AuthN's OAuth Return](api.md#oauth-return) as the redirect URL. Save `Client ID` and `Client Secret`. Join those together with a `:` and provide them to AuthN as a single variable. + + + ### `GITHUB_OAUTH_CREDENTIALS` | | | diff --git a/lib/oauth/twitter.go b/lib/oauth/twitter.go new file mode 100644 index 000000000..66af8719f --- /dev/null +++ b/lib/oauth/twitter.go @@ -0,0 +1,40 @@ +package oauth + +import ( + "context" + "encoding/json" + "io" + + "golang.org/x/oauth2" +) + +// NewTwitterProvider returns a AuthN integration for Twitter OAuth +func NewTwitterProvider(credentials *Credentials) *Provider { + config := &oauth2.Config{ + ClientID: credentials.ID, + ClientSecret: credentials.Secret, + Scopes: []string{"email"}, + Endpoint: oauth2.Endpoint{ + AuthURL: "https://twitter.com/i/oauth2/authorize", + TokenURL: "https://api.twitter.com/2/oauth2/token", + }, + } + + return NewProvider(config, func(t *oauth2.Token) (*UserInfo, error) { + client := config.Client(context.TODO(), t) + resp, err := client.Get("https://api.twitter.com/2/me") + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var user UserInfo + err = json.Unmarshal(body, &user) + return &user, err + }) +}