Skip to content

Commit

Permalink
feat: Add Twitter/X auth
Browse files Browse the repository at this point in the history
  • Loading branch information
dschwertfeger committed Aug 27, 2024
1 parent ec15625 commit e5658b9
Show file tree
Hide file tree
Showing 32 changed files with 243 additions and 17 deletions.
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

0 comments on commit e5658b9

Please sign in to comment.