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

2016/twitter: Add 𝕏 (fka Twitter) auth to social auth providers #2253

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions waspc/cli/src/Wasp/Cli/Command/Studio.hs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ studio = do
[ "discord"
| isJust $ AS.App.Auth.discord methods
],
[ "twitter"
| isJust $ AS.App.Auth.twitter methods
],
[ "google"
| isJust $ AS.App.Auth.google methods
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ const keycloakSignInUrl = `${config.apiUrl}{= keycloakSignInPath =}`
{=# enabledProviders.isGitHubAuthEnabled =}
const gitHubSignInUrl = `${config.apiUrl}{= gitHubSignInPath =}`
{=/ enabledProviders.isGitHubAuthEnabled =}
{=# enabledProviders.isTwitterAuthEnabled =}
const twitterSignInUrl = `${config.apiUrl}{= twitterSignInPath =}`
{=/ enabledProviders.isTwitterAuthEnabled =}

{=!
// Since we allow users to add additional fields to the signup form, we don't
Expand Down Expand Up @@ -208,6 +211,10 @@ export const LoginSignupForm = ({
{=# enabledProviders.isGitHubAuthEnabled =}
<SocialButton href={gitHubSignInUrl}><SocialIcons.GitHub/></SocialButton>
{=/ enabledProviders.isGitHubAuthEnabled =}

{=# enabledProviders.isTwitterAuthEnabled =}
<SocialButton href={twitterSignInUrl}><SocialIcons.Twitter/></SocialButton>
{=/ enabledProviders.isTwitterAuthEnabled =}
</SocialAuthButtons>
</SocialAuth>
{=/ isSocialAuthEnabled =}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,14 @@ export const Discord = () => (
<path d="M13.545 2.907a13.227 13.227 0 00-3.257-1.011.05.05 0 00-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 00-3.658 0 8.258 8.258 0 00-.412-.833.051.051 0 00-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 00-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 003.995 2.02.05.05 0 00.056-.019c.308-.42.582-.863.818-1.329a.05.05 0 00-.01-.059.051.051 0 00-.018-.011 8.875 8.875 0 01-1.248-.595.05.05 0 01-.02-.066.051.051 0 01.015-.019c.084-.063.168-.129.248-.195a.05.05 0 01.051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 01.053.007c.08.066.164.132.248.195a.051.051 0 01-.004.085 8.254 8.254 0 01-1.249.594.05.05 0 00-.03.03.052.052 0 00.003.041c.24.465.515.909.817 1.329a.05.05 0 00.056.019 13.235 13.235 0 004.001-2.02.049.049 0 00.021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 00-.02-.019zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612z" />
</svg>
)

export const Twitter = () => (
<svg
className={defaultStyles()}
aria-hidden="true"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
)
1 change: 1 addition & 0 deletions waspc/data/Generator/templates/sdk/wasp/auth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type PossibleProviderData = {
google: OAuthProviderData;
keycloak: OAuthProviderData;
github: OAuthProviderData;
twitter: OAuthProviderData;
}

// PUBLIC API
Expand Down
3 changes: 3 additions & 0 deletions waspc/data/Generator/templates/sdk/wasp/client/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ export * from './keycloak'
{=# isGitHubAuthEnabled =}
export * from './github'
{=/ isGitHubAuthEnabled =}
{=# isTwitterAuthEnabled =}
export * from './twitter'
{=/ isTwitterAuthEnabled =}
export {
default as useAuth,
getMe,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// PUBLIC API
export { signInUrl as twitterSignInUrl } from '../../auth/helpers/Twitter'
3 changes: 3 additions & 0 deletions waspc/data/Generator/templates/sdk/wasp/client/auth/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export { SignInButton as KeycloakSignInButton } from '../../auth/helpers/Keycloa
{=# isGitHubAuthEnabled =}
export { SignInButton as GitHubSignInButton } from '../../auth/helpers/GitHub'
{=/ isGitHubAuthEnabled =}
{=# isTwitterAuthEnabled =}
export { SignInButton as TwitterSignInButton } from '../../auth/helpers/Twitter'
{=/ isTwitterAuthEnabled =}
export {
FormError,
FormInput,
Expand Down
3 changes: 3 additions & 0 deletions waspc/data/Generator/templates/sdk/wasp/server/auth/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,8 @@ export type OAuthData = {
{=# enabledProviders.isKeycloakAuthEnabled =}
| { providerName: 'keycloak'; tokens: import('arctic').KeycloakTokens }
{=/ enabledProviders.isKeycloakAuthEnabled =}
{=# enabledProviders.isTwitterAuthEnabled =}
| { providerName: 'twitter'; tokens: import('arctic').TwitterTokens }
{=/ enabledProviders.isTwitterAuthEnabled =}
| never
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export { github } from './providers/github.js';
// PUBLIC API
export { keycloak } from './providers/keycloak.js';
{=/ enabledProviders.isKeycloakAuthEnabled =}
{=# enabledProviders.isTwitterAuthEnabled =}
// PUBLIC API
export { twitter } from './providers/twitter.js';
{=/ enabledProviders.isTwitterAuthEnabled =}

// PRIVATE API
export {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{{={= =}=}}
import { Twitter } from "arctic";

import { defineProvider } from "../provider.js";
import { ensureEnvVarsForProvider } from "../env.js";
import { getRedirectUriForCallback } from "../redirect.js";

const id = "{= providerId =}";
const displayName = "{= displayName =}";

const env = ensureEnvVarsForProvider(
["TWITTER_CLIENT_ID", "TWITTER_CLIENT_SECRET"],
displayName
);

const oAuthClient = new Twitter(
env.TWITTER_CLIENT_ID,
env.TWITTER_CLIENT_SECRET,
getRedirectUriForCallback(id).toString(),
);

// PUBLIC API
export const twitter = defineProvider({
id,
displayName,
env,
oAuthClient,
});
6 changes: 6 additions & 0 deletions waspc/data/Generator/templates/sdk/wasp/server/auth/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export type AuthUserData = Omit<CompleteUserEntityWithAuth, '{= authFieldOnUserE
{=# enabledProviders.isGitHubAuthEnabled =}
github: Expand<UserFacingProviderData<'github'>> | null
{=/ enabledProviders.isGitHubAuthEnabled =}
{=# enabledProviders.isTwitterAuthEnabled =}
twitter: Expand<UserFacingProviderData<'twitter'>> | null
{=/ enabledProviders.isTwitterAuthEnabled =}
},
}

Expand Down Expand Up @@ -111,6 +114,9 @@ This should never happen, but it did which means there is a bug in the code.`)
{=# enabledProviders.isGitHubAuthEnabled =}
github: getProviderInfo<'github'>({= authFieldOnUserEntityName =}, 'github'),
{=/ enabledProviders.isGitHubAuthEnabled =}
{=# enabledProviders.isTwitterAuthEnabled =}
twitter: getProviderInfo<'twitter'>({= authFieldOnUserEntityName =}, 'twitter'),
{=/ enabledProviders.isTwitterAuthEnabled =}
}
return {
...rest,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{{={= =}=}}

import type { ProviderConfig } from "wasp/auth/providers/types";
import { twitter } from "wasp/server/auth";
import { mergeDefaultAndUserConfig } from "../oauth/config.js";
import { createOAuthProviderRouter } from "../oauth/handler.js";

{=# userSignupFields.isDefined =}
{=& userSignupFields.importStatement =}
const _waspUserSignupFields = {= userSignupFields.importIdentifier =}
{=/ userSignupFields.isDefined =}
{=^ userSignupFields.isDefined =}
const _waspUserSignupFields = undefined
{=/ userSignupFields.isDefined =}
{=# configFn.isDefined =}
{=& configFn.importStatement =}
const _waspUserDefinedConfigFn = {= configFn.importIdentifier =}
{=/ configFn.isDefined =}
{=^ configFn.isDefined =}
const _waspUserDefinedConfigFn = undefined
{=/ configFn.isDefined =}

const _waspConfig: ProviderConfig = {
id: twitter.id,
displayName: twitter.displayName,
createRouter(provider) {
const config = mergeDefaultAndUserConfig({
scopes: {=& requiredScopes =},
}, _waspUserDefinedConfigFn);

async function getTwitterProfile(accessToken: string): Promise<{
providerProfile: unknown;
providerUserId: string;
}> {
const response = await fetch("https://api.twitter.com/2/users/me", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});

const jsonResponse = await response.json();

const providerProfile = jsonResponse.data as {
id?: string;
name?: string;
username?: string;
};


if (!providerProfile.id) {
throw new Error("Invalid profile");
}

return { providerProfile, providerUserId: providerProfile.id };
}

return createOAuthProviderRouter({
provider,
oAuthType: 'OAuth2WithPKCE',
userSignupFields: _waspUserSignupFields,
getAuthorizationUrl: ({ state, codeVerifier }) => twitter.oAuthClient.createAuthorizationURL(state, codeVerifier, config),
getProviderTokens: ({ code, codeVerifier }) => twitter.oAuthClient.validateAuthorizationCode(code, codeVerifier),
getProviderInfo: ({ accessToken }) => getTwitterProfile(accessToken),
});
},
}

export default _waspConfig;
4 changes: 4 additions & 0 deletions waspc/examples/todoApp/.env.server.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ GITHUB_CLIENT_SECRET='dummy-gh-client-secret'
# Dummy values here will allow app to run, but you will need real values to get Discord Auth to work.
DISCORD_CLIENT_SECRET='dummy-discord-client-secret'
DISCORD_CLIENT_ID='dummy-discord-client-id'

# Dummy values here will allow app to run, but you will need real values to get Twitter Auth to work.
TWITTER_CLIENT_SECRET='dummy-twitter-client-secret'
TWITTER_CLIENT_ID='dummy-twitter-client-id'
4 changes: 4 additions & 0 deletions waspc/examples/todoApp/main.wasp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ app todoApp {
configFn: import { config } from "@src/auth/github.js",
userSignupFields: import { userSignupFields } from "@src/auth/github.js"
},
twitter: {
configFn: import { config } from "@src/auth/twitter.js",
userSignupFields: import { userSignupFields } from "@src/auth/twitter.js"
},
// keycloak: {},
email: {
userSignupFields: import { userSignupFields } from "@src/auth/email",
Expand Down
10 changes: 10 additions & 0 deletions waspc/examples/todoApp/src/auth/twitter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineUserSignupFields } from 'wasp/server/auth'

export function config() {
console.log('Inside user-supplied Twitter config')
return {
scopes: ['users.read', 'tweet.read'],
}
}

export const userSignupFields = defineUserSignupFields({})
8 changes: 7 additions & 1 deletion waspc/src/Wasp/AppSpec/App/Auth.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module Wasp.AppSpec.App.Auth
isKeycloakAuthEnabled,
isGitHubAuthEnabled,
isEmailAuthEnabled,
isTwitterAuthEnabled,
userSignupFieldsForEmailAuth,
userSignupFieldsForUsernameAuth,
userSignupFieldsForExternalAuth,
Expand Down Expand Up @@ -49,6 +50,7 @@ data AuthMethods = AuthMethods
google :: Maybe ExternalAuthConfig,
gitHub :: Maybe ExternalAuthConfig,
keycloak :: Maybe ExternalAuthConfig,
twitter :: Maybe ExternalAuthConfig,
email :: Maybe EmailAuthConfig
}
deriving (Show, Eq, Data)
Expand Down Expand Up @@ -83,7 +85,8 @@ isExternalAuthEnabled auth =
[ isDiscordAuthEnabled,
isGoogleAuthEnabled,
isGitHubAuthEnabled,
isKeycloakAuthEnabled
isKeycloakAuthEnabled,
isTwitterAuthEnabled
]

isDiscordAuthEnabled :: Auth -> Bool
Expand All @@ -98,6 +101,9 @@ isKeycloakAuthEnabled = isJust . keycloak . methods
isGitHubAuthEnabled :: Auth -> Bool
isGitHubAuthEnabled = isJust . gitHub . methods

isTwitterAuthEnabled :: Auth -> Bool
isTwitterAuthEnabled = isJust . twitter . methods

isEmailAuthEnabled :: Auth -> Bool
isEmailAuthEnabled = isJust . email . methods

Expand Down
9 changes: 9 additions & 0 deletions waspc/src/Wasp/Generator/AuthProviders.hs
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,22 @@ discordAuthProvider =
OA._requiredScope = ["identify"]
}

twitterAuthProvider :: OA.OAuthAuthProvider
twitterAuthProvider =
OA.OAuthAuthProvider
{ OA._providerId = fromJust $ makeProviderId "twitter",
OA._displayName = "Twitter",
OA._requiredScope = ["users.read", "tweet.read"]
}

getEnabledAuthProvidersJson :: AS.Auth.Auth -> Aeson.Value
getEnabledAuthProvidersJson auth =
object
[ "isDiscordAuthEnabled" .= AS.Auth.isDiscordAuthEnabled auth,
"isGoogleAuthEnabled" .= AS.Auth.isGoogleAuthEnabled auth,
"isKeycloakAuthEnabled" .= AS.Auth.isKeycloakAuthEnabled auth,
"isGitHubAuthEnabled" .= AS.Auth.isGitHubAuthEnabled auth,
"isTwitterAuthEnabled" .= AS.Auth.isTwitterAuthEnabled auth,
"isUsernameAndPasswordAuthEnabled" .= AS.Auth.isUsernameAndPasswordAuthEnabled auth,
"isEmailAuthEnabled" .= AS.Auth.isEmailAuthEnabled auth
]
2 changes: 2 additions & 0 deletions waspc/src/Wasp/Generator/SdkGenerator/Auth/AuthFormsG.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Wasp.Generator.AuthProviders
gitHubAuthProvider,
googleAuthProvider,
keycloakAuthProvider,
twitterAuthProvider,
)
import qualified Wasp.Generator.AuthProviders as AuthProviders
import qualified Wasp.Generator.AuthProviders.OAuth as OAuth
Expand Down Expand Up @@ -124,6 +125,7 @@ genLoginSignupForm auth =
"googleSignInPath" .= OAuth.serverLoginUrl googleAuthProvider,
"keycloakSignInPath" .= OAuth.serverLoginUrl keycloakAuthProvider,
"gitHubSignInPath" .= OAuth.serverLoginUrl gitHubAuthProvider,
"twitterSignInPath" .= OAuth.serverLoginUrl twitterAuthProvider,
"enabledProviders" .= AuthProviders.getEnabledAuthProvidersJson auth
]
areBothSocialAndPasswordBasedAuthEnabled = AS.Auth.isExternalAuthEnabled auth && isAnyPasswordBasedAuthEnabled
Expand Down
5 changes: 4 additions & 1 deletion waspc/src/Wasp/Generator/SdkGenerator/Auth/OAuthAuthG.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Wasp.Generator.AuthProviders
gitHubAuthProvider,
googleAuthProvider,
keycloakAuthProvider,
twitterAuthProvider,
)
import Wasp.Generator.AuthProviders.OAuth (OAuthAuthProvider)
import qualified Wasp.Generator.AuthProviders.OAuth as OAuth
Expand All @@ -32,13 +33,15 @@ genHelpers auth =
[ [discordHelpers | AS.Auth.isDiscordAuthEnabled auth],
[gitHubHelpers | AS.Auth.isGitHubAuthEnabled auth],
[googleHelpers | AS.Auth.isGoogleAuthEnabled auth],
[keycloakHelpers | AS.Auth.isKeycloakAuthEnabled auth]
[keycloakHelpers | AS.Auth.isKeycloakAuthEnabled auth],
[twitterHelpers | AS.Auth.isTwitterAuthEnabled auth]
]
where
discordHelpers = mkHelpersFd discordAuthProvider [relfile|Discord.tsx|]
gitHubHelpers = mkHelpersFd gitHubAuthProvider [relfile|GitHub.tsx|]
googleHelpers = mkHelpersFd googleAuthProvider [relfile|Google.tsx|]
keycloakHelpers = mkHelpersFd keycloakAuthProvider [relfile|Keycloak.tsx|]
twitterHelpers = mkHelpersFd twitterAuthProvider [relfile|Twitter.tsx|]

mkHelpersFd :: OAuthAuthProvider -> Path' Rel' File' -> FileDraft
mkHelpersFd provider helpersFp =
Expand Down
7 changes: 7 additions & 0 deletions waspc/src/Wasp/Generator/SdkGenerator/Client/AuthG.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ genNewClientAuth spec =
<++> genAuthGoogle auth
<++> genAuthKeycloak auth
<++> genAuthGitHub auth
<++> genAuthTwitter auth
where
maybeAuth = AS.App.auth $ snd $ getApp spec

Expand Down Expand Up @@ -87,5 +88,11 @@ genAuthGitHub auth =
then sequence [genFileCopy [relfile|client/auth/github.ts|]]
else return []

genAuthTwitter :: AS.Auth.Auth -> Generator [FileDraft]
genAuthTwitter auth =
if AS.Auth.isTwitterAuthEnabled auth
then sequence [genFileCopy [relfile|client/auth/twitter.ts|]]
else return []

genFileCopy :: Path' (Rel SdkTemplatesDir) File' -> Generator FileDraft
genFileCopy = return . C.mkTmplFd
3 changes: 2 additions & 1 deletion waspc/src/Wasp/Generator/SdkGenerator/Server/OAuthG.hs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import qualified Wasp.AppSpec.App.Auth as AS.App.Auth
import qualified Wasp.AppSpec.App.Auth as AS.Auth
import qualified Wasp.AppSpec.App.Dependency as AS.Dependency
import qualified Wasp.AppSpec.Valid as AS.Valid
import Wasp.Generator.AuthProviders (discordAuthProvider, getEnabledAuthProvidersJson, gitHubAuthProvider, googleAuthProvider, keycloakAuthProvider)
import Wasp.Generator.AuthProviders (discordAuthProvider, getEnabledAuthProvidersJson, gitHubAuthProvider, googleAuthProvider, keycloakAuthProvider, twitterAuthProvider)
import Wasp.Generator.AuthProviders.OAuth
( OAuthAuthProvider,
clientOAuthCallbackPath,
Expand Down Expand Up @@ -43,6 +43,7 @@ genOAuth auth
<++> genOAuthProvider googleAuthProvider (AS.Auth.google . AS.Auth.methods $ auth)
<++> genOAuthProvider keycloakAuthProvider (AS.Auth.keycloak . AS.Auth.methods $ auth)
<++> genOAuthProvider gitHubAuthProvider (AS.Auth.gitHub . AS.Auth.methods $ auth)
<++> genOAuthProvider twitterAuthProvider (AS.Auth.twitter . AS.Auth.methods $ auth)
| otherwise = return []
where
genFileCopy = return . C.mkTmplFd
Expand Down
2 changes: 2 additions & 0 deletions waspc/src/Wasp/Generator/ServerGenerator/Auth/OAuthAuthG.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import Wasp.Generator.AuthProviders
gitHubAuthProvider,
googleAuthProvider,
keycloakAuthProvider,
twitterAuthProvider,
)
import Wasp.Generator.AuthProviders.OAuth (OAuthAuthProvider)
import qualified Wasp.Generator.AuthProviders.OAuth as OAuth
Expand All @@ -45,6 +46,7 @@ genOAuthAuth auth
<++> genOAuthProvider googleAuthProvider (AS.Auth.google . AS.Auth.methods $ auth)
<++> genOAuthProvider keycloakAuthProvider (AS.Auth.keycloak . AS.Auth.methods $ auth)
<++> genOAuthProvider gitHubAuthProvider (AS.Auth.gitHub . AS.Auth.methods $ auth)
<++> genOAuthProvider twitterAuthProvider (AS.Auth.twitter . AS.Auth.methods $ auth)
| otherwise = return []

genOAuthHelpers :: AS.Auth.Auth -> Generator [FileDraft]
Expand Down
Loading
Loading