feat(next): add email, google, apple sign in

Because:

- On the checkout page, after populating the email clicking continue
  should navigate the user to the FxA signin page for signin.
- On the checkout page, when clicking Google/Apple button, it should
  start the third party auth via FxA.
- Completion of either signin option should navigate the user back to
  the checkout page in a signed in state.

This commit:

- Updated Auth.js provider to support email, google and apple login
- Customers are redirected to the '/new' page on successful login, which
  creates a new cart with the customers uid added.
- Updates Payments Next auth config to a non-public client.

Closes #FXA-10548
This commit is contained in:
Reino Muhl 2024-11-14 15:22:12 -05:00
Родитель a1b137c04f
Коммит 182c00b3c1
Не найден ключ, соответствующий данной подписи
11 изменённых файлов: 163 добавлений и 50 удалений

Просмотреть файл

@ -6,13 +6,17 @@
# Auth
AUTH__ISSUER_URL=http://localhost:3030
AUTH__WELL_KNOWN_URL=http://localhost:3030/.well-known/openid-configuration
AUTH__TOKEN_URL=http://localhost:9000/v1/token
AUTH__USERINFO_URL=http://localhost:1111/v1/profile
AUTH__CLIENT_ID=32aaeb6f1c21316a
AUTH__CLIENT_SECRET=024f261551f122335424aee381701615a659b91a6f2017429a5832c231c19f9a
# PayPal
PAYPAL__CLIENT_ID=sb
# NextAuth
AUTH_SECRET=replacewithsecret
AUTH_TRUST_HOST=true
# MySQLConfig
MYSQL_CONFIG__DATABASE=fxa
@ -106,6 +110,7 @@ PROFILE_CLIENT_CONFIG__SERVICE_NAME='subhub'
# Other
CONTENT_SERVER_URL=http://localhost:3030
SUPPORT_URL=https://support.mozilla.org
BASE_URL=http://localhost:3035
# Nextjs Public Environment Variables

Просмотреть файл

@ -6,13 +6,17 @@
# Auth
AUTH__ISSUER_URL=http://localhost:3030
AUTH__WELL_KNOWN_URL=http://localhost:3030/.well-known/openid-configuration
AUTH__TOKEN_URL=http://localhost:9000/v1/token
AUTH__USERINFO_URL=http://localhost:1111/v1/profile
AUTH__CLIENT_ID=
AUTH__CLIENT_SECRET=placeholder
# PayPal
PAYPAL__CLIENT_ID=sb
# NextAuth
AUTH_SECRET=placeholder
AUTH_TRUST_HOST=true
# MySQLConfig
MYSQL_CONFIG__DATABASE=fxa
@ -102,6 +106,7 @@ PROFILE_CLIENT_CONFIG__SERVICE_NAME='subhub'
# Other
CONTENT_SERVER_URL=https://accounts.firefox.com
SUPPORT_URL=https://support.mozilla.org
BASE_URL=http://localhost:3035
# Nextjs Public Environment Variables

Просмотреть файл

@ -6,6 +6,7 @@ import { headers } from 'next/headers';
import Image from 'next/image';
import {
BaseButton,
buildRedirectUrl,
ButtonVariant,
PaymentSection,
SignInForm,
@ -19,12 +20,22 @@ import {
import AppleLogo from '@fxa/shared/assets/images/apple-logo.svg';
import GoogleLogo from '@fxa/shared/assets/images/google-logo.svg';
import { DEFAULT_LOCALE } from '@fxa/shared/l10n';
import { auth } from 'apps/payments/next/auth';
import { fetchCMSData, getCartOrRedirectAction } from '@fxa/payments/ui/actions';
import { auth, signIn } from 'apps/payments/next/auth';
import {
fetchCMSData,
getCartOrRedirectAction,
} from '@fxa/payments/ui/actions';
import { config } from 'apps/payments/next/config';
export const dynamic = 'force-dynamic';
export default async function Checkout({ params }: { params: CheckoutParams }) {
export default async function Checkout({
params,
searchParams,
}: {
params: CheckoutParams;
searchParams: Record<string, string> | undefined;
}) {
// Temporarily defaulting to `accept-language`
// This to be updated in FXA-9404
//const locale = getLocaleFromRequest(
@ -46,9 +57,17 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
cmsDataPromise,
]);
const redirectTo = buildRedirectUrl(
params.offeringId,
params.interval,
'new',
'checkout',
{ baseUrl: config.baseUrl, locale: params.locale, searchParams }
);
return (
<section aria-label="Checkout">
{!session && (
{!session?.user && (
<>
<h2 className="font-semibold text-grey-600 text-lg mt-10">
{l10n.getString(
@ -58,6 +77,13 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
</h2>
<SignInForm
submitAction={async (email?: string) => {
'use server';
const additionalParams = email
? { login_hint: email }
: undefined;
await signIn('fxa', { redirectTo }, additionalParams);
}}
newsletterLabel={cms.commonContent.newsletterLabelTextCode}
/>
@ -69,6 +95,16 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
</h3>
<div className="flex flex-col gap-4 mt-6 mb-10 desktop:flex-row desktop:items-center desktop:justify-center">
<form
action={async () => {
'use server';
await signIn(
'fxa',
{ redirectTo },
{ deeplink: 'googleLogin' }
);
}}
>
<BaseButton variant={ButtonVariant.ThirdParty}>
<Image src={GoogleLogo} alt="" />
{l10n.getString(
@ -76,6 +112,13 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
'Continue with Google'
)}
</BaseButton>
</form>
<form
action={async () => {
'use server';
await signIn('fxa', { redirectTo }, { deeplink: 'appleLogin' });
}}
>
<BaseButton variant={ButtonVariant.ThirdParty}>
<Image src={AppleLogo} alt="" />
{l10n.getString(
@ -83,6 +126,7 @@ export default async function Checkout({ params }: { params: CheckoutParams }) {
'Continue with Apple'
)}
</BaseButton>
</form>
</div>
<hr className="mx-auto w-full border-grey-200" />

Просмотреть файл

@ -17,15 +17,15 @@ export const {
{
id: 'fxa',
name: 'Firefox Accounts',
type: 'oidc',
type: 'oauth',
issuer: config.auth.issuerUrl,
wellKnown: config.auth.wellKnownUrl,
checks: ['pkce', 'state'],
client: {
token_endpoint_auth_method: 'none',
},
authorization: { params: { scope: 'openid email profile' } },
checks: ['state'],
authorization: { params: { scope: 'email profile' } },
clientId: config.auth.clientId,
clientSecret: config.auth.clientSecret,
token: config.auth.tokenUrl,
userinfo: config.auth.userinfoUrl,
},
],
callbacks: {
@ -33,8 +33,9 @@ export const {
params.session.user.id = params.token.fxaUid;
return params.session;
},
async jwt({ token, account }) {
const fxaUid = token.fxaUid || account?.providerAccountId;
async jwt({ token, profile }) {
// Note profile is only defined once after user sign in.
const fxaUid = token.fxaUid || profile?.uid;
return {
...token,
fxaUid,

Просмотреть файл

@ -12,6 +12,7 @@ import {
IsUrl,
IsNumber,
IsOptional,
IsBoolean,
} from 'class-validator';
import {
RootConfig as NestAppRootConfig,
@ -46,8 +47,17 @@ class AuthJSConfig {
@IsUrl({ require_tld: false })
wellKnownUrl!: string;
@IsUrl({ require_tld: false })
tokenUrl!: string;
@IsUrl({ require_tld: false })
userinfoUrl!: string;
@IsString()
clientId!: string;
@IsString()
clientSecret!: string;
}
export class PaymentsNextConfig extends NestAppRootConfig {
@ -74,12 +84,18 @@ export class PaymentsNextConfig extends NestAppRootConfig {
@IsString()
authSecret!: string;
@IsBoolean()
authTrustHost!: boolean;
@IsString()
stripePublicApiKey!: string;
@IsUrl({ require_tld: false })
contentServerUrl!: string;
@IsUrl({ require_tld: false })
baseUrl!: string;
/**
* Nextjs Public Environment Variables
*/

Просмотреть файл

@ -22,3 +22,4 @@ export * from './lib/utils/helpers';
export * from './lib/utils/types';
export * from './lib/utils/get-cart';
export * from './lib/utils/poll-cart';
export * from './lib/utils/buildRedirectUrl';

Просмотреть файл

@ -38,14 +38,23 @@ function getNewsletterStringInfo(newsletterLabelTextCode?: string | null) {
}
interface SignInFormProps {
submitAction: (email?: string) => Promise<void>;
newsletterLabel?: string | null;
}
export const SignInForm = ({ newsletterLabel }: SignInFormProps) => {
export const SignInForm = ({
newsletterLabel,
submitAction,
}: SignInFormProps) => {
const { newsletterStringId, newsletterStringFallbackText } =
getNewsletterStringInfo(newsletterLabel);
return (
<Form.Root aria-label="Sign-in/sign-up form">
<Form.Root
action={async (formData: FormData) =>
submitAction(formData.get('email')?.toString())
}
aria-label="Sign-in/sign-up form"
>
<Form.Field name="email" className="my-6">
<Form.Label className="text-grey-400 block mb-1 text-start">
<Localized id="checkout-enter-your-email">Enter your email</Localized>
@ -54,6 +63,7 @@ export const SignInForm = ({ newsletterLabel }: SignInFormProps) => {
<input
className="w-full border rounded-md border-black/30 p-3 placeholder:text-grey-500 placeholder:font-normal focus:border focus:!border-black/30 focus:!shadow-[0_0_0_3px_rgba(10,132,255,0.3)] focus-visible:outline-none data-[invalid=true]:border-alert-red data-[invalid=true]:text-alert-red data-[invalid=true]:shadow-inputError"
type="email"
name="email"
data-testid="email"
required
aria-required

Просмотреть файл

@ -0,0 +1,32 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
type Page = 'landing' | 'new' | 'start' | 'success' | 'error';
type PageType = 'checkout' | 'upgrade';
interface Optional {
baseUrl?: string;
locale?: string;
cartId?: string;
searchParams?: Record<string, string>;
}
export function buildRedirectUrl(
offeringId: string,
interval: string,
page: Page,
pageType: PageType,
optional?: Optional
) {
const baseUrl = optional?.baseUrl ? optional?.baseUrl : '';
const startUrl = baseUrl === '/' ? baseUrl : `${baseUrl}/`;
const endUrl = optional?.cartId ? `${optional?.cartId}/${page}` : page;
const searchParamsString = optional?.searchParams
? `?${new URLSearchParams(optional?.searchParams).toString()}`
: '';
return `${startUrl}${optional?.locale}/${offeringId}/${pageType}/${interval}/${endUrl}${searchParamsString}`;
}

Просмотреть файл

@ -100,7 +100,7 @@
"nest-typed-config": "^2.9.2",
"nest-winston": "^1.10.0",
"next": "^14.2.14",
"next-auth": "beta",
"next-auth": "5.0.0-beta.22",
"node-fetch": "^2.6.7",
"node-hkdf": "^0.0.2",
"node-jose": "^2.2.0",

Просмотреть файл

@ -369,15 +369,14 @@
{
"id": "32aaeb6f1c21316a",
"name": "Firefox Accounts Payments Next",
"hashedSecret": "b312c769bfce7682c2482c701199d88a8b67e40cee43cbe03bafe6eca47fff75",
"hashedSecret": "674609e00c64cedb4b486caf5ed3914f1fa86be25cc5cc6927a8a3d1c6981c4e",
"redirectUri": "http://localhost:3035/api/auth/callback/fxa",
"imageUri": "",
"canGrant": true,
"termsUri": "",
"privacyUri": "",
"trusted": true,
"allowedScopes": "https://identity.mozilla.com/account/subscriptions https://identity.mozilla.com/account/newsletters",
"publicClient": true
"allowedScopes": "https://identity.mozilla.com/account/subscriptions https://identity.mozilla.com/account/newsletters"
},
{
"name": "Thunderbird",

Просмотреть файл

@ -528,15 +528,15 @@ __metadata:
languageName: node
linkType: hard
"@auth/core@npm:0.37.2":
version: 0.37.2
resolution: "@auth/core@npm:0.37.2"
"@auth/core@npm:0.35.3":
version: 0.35.3
resolution: "@auth/core@npm:0.35.3"
dependencies:
"@panva/hkdf": ^1.2.1
"@types/cookie": 0.6.0
cookie: 0.7.1
jose: ^5.9.3
oauth4webapi: ^3.0.0
cookie: 0.6.0
jose: ^5.1.3
oauth4webapi: ^2.10.4
preact: 10.11.3
preact-render-to-string: 5.2.3
peerDependencies:
@ -550,7 +550,7 @@ __metadata:
optional: true
nodemailer:
optional: true
checksum: 669eea9bf2d84e97d179f2dd0b7f7915269aa23d6014d0dd177f072bf3f894f1a6844a346bcdea868325976cc064baa6c26925715447ba61aa96875782458969
checksum: 93b2db76c80807598f56aaf685cbd1ad9df083a7859ab58a24b55e422683e53c5497d814fc71255b22207275eef99aff253d6698027ae1f03f35ed567b2c907c
languageName: node
linkType: hard
@ -41996,7 +41996,7 @@ fsevents@~2.1.1:
nest-typed-config: ^2.9.2
nest-winston: ^1.10.0
next: ^14.2.14
next-auth: beta
next-auth: 5.0.0-beta.22
node-fetch: ^2.6.7
node-hkdf: ^0.0.2
node-jose: ^2.2.0
@ -49290,7 +49290,7 @@ fsevents@~2.1.1:
languageName: node
linkType: hard
"jose@npm:^5.9.3, jose@npm:^5.9.6":
"jose@npm:^5.1.3, jose@npm:^5.9.6":
version: 5.9.6
resolution: "jose@npm:5.9.6"
checksum: 4b536da0201858ed4c4582e8bb479081f11e0c63dd0f5e473adde16fc539785e1f2f0409bc1fc7cbbb5b68026776c960b4952da3a06f6fdfff0b9764c9127ae0
@ -54004,11 +54004,11 @@ fsevents@~2.1.1:
languageName: node
linkType: hard
"next-auth@npm:beta":
version: 5.0.0-beta.25
resolution: "next-auth@npm:5.0.0-beta.25"
"next-auth@npm:5.0.0-beta.22":
version: 5.0.0-beta.22
resolution: "next-auth@npm:5.0.0-beta.22"
dependencies:
"@auth/core": 0.37.2
"@auth/core": 0.35.3
peerDependencies:
"@simplewebauthn/browser": ^9.0.1
"@simplewebauthn/server": ^9.0.2
@ -54022,7 +54022,7 @@ fsevents@~2.1.1:
optional: true
nodemailer:
optional: true
checksum: 5a3f52e6d32ae6c954e438496b96bb8d46ba60ca6dbfab146af718d50e7fe8b967b7019fda162e5fa32bfc5219160f7b995c327d62d5144e7935bb18b9b61e39
checksum: 4b82f3fc147b444ec4942900c0e0a430372117e4c2c2ad84dff0afbce5dea4f36ef147e647f8b88e9894b492d8a92faa26801203d2d2839541092713c8767866
languageName: node
linkType: hard
@ -55243,10 +55243,10 @@ fsevents@~2.1.1:
languageName: node
linkType: hard
"oauth4webapi@npm:^3.0.0":
version: 3.1.2
resolution: "oauth4webapi@npm:3.1.2"
checksum: cd986eba0ee3d8853a4abb1ee3296cafa876cc9a8d2716213d01a3d396f1758581d1fe83e8b9fb1be16c226b0e1392ce528f06558532082ea2a928bb244407d8
"oauth4webapi@npm:^2.10.4":
version: 2.17.0
resolution: "oauth4webapi@npm:2.17.0"
checksum: 2a29c1d4c2f597aa8e41f51bc102cf098c23295a05b0ab4769f53800b6ede7abae3f8b00136634e1ec32bea7849cbefdd9e0f21ec7c215f45fa6d9e82cb6701c
languageName: node
linkType: hard