From 52d9f99715727d4782a9bdd99b792ef90508e356 Mon Sep 17 00:00:00 2001 From: Robin Wieruch Date: Thu, 16 Apr 2020 12:07:00 +0200 Subject: [PATCH] session resolver authorization --- pages/api/graphql.ts | 4 +- src/api/authorization/index.ts | 3 +- src/api/middleware/{ => global}/me.ts | 2 +- src/api/middleware/{ => global}/sentry.ts | 0 .../middleware/resolver/isAuthenticated.ts | 15 +++++ src/api/resolvers/session/index.ts | 64 +++++++++++-------- src/generated/client.tsx | 16 ++--- src/queries/session.ts | 4 +- .../PasswordChangeForm/index.tsx | 3 - src/screens/SignIn/SignInForm/index.tsx | 2 +- src/screens/SignIn/SignInForm/spec.tsx | 2 +- src/screens/SignUp/SignUpForm/index.tsx | 2 +- src/screens/SignUp/SignUpForm/spec.tsx | 2 +- 13 files changed, 72 insertions(+), 47 deletions(-) rename src/api/middleware/{ => global}/me.ts (93%) rename src/api/middleware/{ => global}/sentry.ts (100%) create mode 100644 src/api/middleware/resolver/isAuthenticated.ts diff --git a/pages/api/graphql.ts b/pages/api/graphql.ts index cebe9040..9c92af4c 100644 --- a/pages/api/graphql.ts +++ b/pages/api/graphql.ts @@ -15,8 +15,8 @@ import { ServerRequest, ServerResponse } from '@typeDefs/server'; import { ResolverContext } from '@typeDefs/resolver'; import resolvers from '@api/resolvers'; -import meMiddleware from '@api/middleware/me'; -import sentryMiddleware from '@api/middleware/sentry'; +import meMiddleware from '@api/middleware/global/me'; +import sentryMiddleware from '@api/middleware/global/sentry'; import firebaseAdmin from '@services/firebase/admin'; diff --git a/src/api/authorization/index.ts b/src/api/authorization/index.ts index 2fd3ac1f..a00b8bac 100644 --- a/src/api/authorization/index.ts +++ b/src/api/authorization/index.ts @@ -17,7 +17,8 @@ export default shield({ partnerPayments: and(isAuthenticated, isPartner), }, Mutation: { - passwordChange: isAuthenticated, + // passwordChange: isAuthenticated, + // emailChange: isAuthenticated, communityJoin: isAuthenticated, // Admin diff --git a/src/api/middleware/me.ts b/src/api/middleware/global/me.ts similarity index 93% rename from src/api/middleware/me.ts rename to src/api/middleware/global/me.ts index e72497bc..bb73b495 100644 --- a/src/api/middleware/me.ts +++ b/src/api/middleware/global/me.ts @@ -15,7 +15,7 @@ export default async ( const { session } = context.req.cookies; if (!session) { - return undefined; + return await resolve(root, args, context, info); } const CHECK_REVOKED = true; diff --git a/src/api/middleware/sentry.ts b/src/api/middleware/global/sentry.ts similarity index 100% rename from src/api/middleware/sentry.ts rename to src/api/middleware/global/sentry.ts diff --git a/src/api/middleware/resolver/isAuthenticated.ts b/src/api/middleware/resolver/isAuthenticated.ts new file mode 100644 index 00000000..61eb28fb --- /dev/null +++ b/src/api/middleware/resolver/isAuthenticated.ts @@ -0,0 +1,15 @@ +import { MiddlewareFn } from 'type-graphql'; +import { ForbiddenError } from 'apollo-server'; + +import { ResolverContext } from '@typeDefs/resolver'; + +export const isAuthenticated: MiddlewareFn = async ( + { context }, + next +) => { + if (!context.me) { + return new ForbiddenError('Not authenticated as user.'); + } + + return next(); +}; diff --git a/src/api/resolvers/session/index.ts b/src/api/resolvers/session/index.ts index 7f1c0937..00d112c0 100644 --- a/src/api/resolvers/session/index.ts +++ b/src/api/resolvers/session/index.ts @@ -5,6 +5,7 @@ import { Ctx, Resolver, Mutation, + UseMiddleware, } from 'type-graphql'; import { ResolverContext } from '@typeDefs/resolver'; @@ -13,11 +14,12 @@ import firebase from '@services/firebase/client'; import firebaseAdmin from '@services/firebase/admin'; import { inviteToRevue } from '@services/revue'; import { inviteToConvertkit } from '@services/convertkit'; +import { isAuthenticated } from '@api/middleware/resolver/isAuthenticated'; @ObjectType() class SessionToken { @Field() - sessionToken: string; + token: string; } @Resolver() @@ -26,7 +28,7 @@ export default class SessionResolver { async signIn( @Arg('email') email: string, @Arg('password') password: string - ) { + ): Promise { let result; try { @@ -34,20 +36,29 @@ export default class SessionResolver { .auth() .signInWithEmailAndPassword(email, password); } catch (error) { - return new Error(error); + throw new Error(error); + } + + if (!result.user) { + throw new Error('No user found.'); } - const idToken = await result.user?.getIdToken(); - const sessionToken = await firebaseAdmin + const idToken = await result.user.getIdToken(); + + const token = await firebaseAdmin .auth() - .createSessionCookie(idToken || '', { + .createSessionCookie(idToken, { expiresIn: EXPIRES_IN, }); + if (!token) { + throw new Error('Not able to create a session cookie.'); + } + // We manage the session ourselves. await firebase.auth().signOut(); - return { sessionToken }; + return { token }; } @Mutation(() => SessionToken) @@ -63,23 +74,22 @@ export default class SessionResolver { displayName: username, }); } catch (error) { - if (error.message.includes('email address is already in use')) { - return new Error( - 'You already registered with this email. Hint: Check your password manager for our old domain: roadtoreact.com' - ); - } else { - return new Error(error); - } + throw new Error(error); } const { user } = await firebase .auth() .signInWithEmailAndPassword(email, password); - const idToken = await user?.getIdToken(); - const sessionToken = await firebaseAdmin + if (!user) { + throw new Error('No user found.'); + } + + const idToken = await user.getIdToken(); + + const token = await firebaseAdmin .auth() - .createSessionCookie(idToken || '', { + .createSessionCookie(idToken, { expiresIn: EXPIRES_IN, }); @@ -98,47 +108,49 @@ export default class SessionResolver { console.log(error); } - return { sessionToken }; + return { token }; } - @Mutation(() => Boolean, { nullable: true }) + @Mutation(() => Boolean) async passwordForgot(@Arg('email') email: string) { try { await firebase.auth().sendPasswordResetEmail(email); } catch (error) { - return new Error(error); + throw new Error(error); } return true; } - @Mutation(() => Boolean, { nullable: true }) + @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated) async passwordChange( @Arg('password') password: string, @Ctx() ctx: ResolverContext ) { try { - await firebaseAdmin.auth().updateUser(ctx.me?.uid || '', { + await firebaseAdmin.auth().updateUser(ctx.me!.uid, { password, }); } catch (error) { - return new Error(error); + throw new Error(error); } return true; } - @Mutation(() => Boolean, { nullable: true }) + @Mutation(() => Boolean) + @UseMiddleware(isAuthenticated) async emailChange( @Arg('email') email: string, @Ctx() ctx: ResolverContext ) { try { - await firebaseAdmin.auth().updateUser(ctx.me?.uid || '', { + await firebaseAdmin.auth().updateUser(ctx.me!.uid, { email, }); } catch (error) { - return new Error(error); + throw new Error(error); } return true; diff --git a/src/generated/client.tsx b/src/generated/client.tsx index 4ac48625..6347986e 100644 --- a/src/generated/client.tsx +++ b/src/generated/client.tsx @@ -121,9 +121,9 @@ export type Mutation = { migrate: Scalars['Boolean']; signIn: SessionToken; signUp: SessionToken; - passwordForgot?: Maybe; - passwordChange?: Maybe; - emailChange?: Maybe; + passwordForgot: Scalars['Boolean']; + passwordChange: Scalars['Boolean']; + emailChange: Scalars['Boolean']; paypalCreateOrder: PaypalOrderId; paypalApproveOrder: Scalars['Boolean']; stripeCreateOrder: StripeId; @@ -347,7 +347,7 @@ export type QueryPartnerSalesArgs = { export type SessionToken = { __typename?: 'SessionToken'; - sessionToken: Scalars['String']; + token: Scalars['String']; }; export type StorefrontBundle = { @@ -682,7 +682,7 @@ export type SignUpMutation = ( { __typename?: 'Mutation' } & { signUp: ( { __typename?: 'SessionToken' } - & Pick + & Pick ) } ); @@ -696,7 +696,7 @@ export type SignInMutation = ( { __typename?: 'Mutation' } & { signIn: ( { __typename?: 'SessionToken' } - & Pick + & Pick ) } ); @@ -1435,7 +1435,7 @@ export type PaypalApproveOrderMutationOptions = ApolloReactCommon.BaseMutationOp export const SignUpDocument = gql` mutation SignUp($username: String!, $email: String!, $password: String!) { signUp(username: $username, email: $email, password: $password) { - sessionToken + token } } `; @@ -1469,7 +1469,7 @@ export type SignUpMutationOptions = ApolloReactCommon.BaseMutationOptions { const { successMessage } = useIndicators({ key: 'password-change', error, - success: { - message: 'Success! Check your email inbox.', - }, }); const [ diff --git a/src/screens/SignIn/SignInForm/index.tsx b/src/screens/SignIn/SignInForm/index.tsx index dce538c4..3df8e77b 100644 --- a/src/screens/SignIn/SignInForm/index.tsx +++ b/src/screens/SignIn/SignInForm/index.tsx @@ -57,7 +57,7 @@ const SignInForm = ({ }, }); - cookie.set('session', data?.signIn.sessionToken || '', { + cookie.set('session', data?.signIn.token || '', { expires: EXPIRES_IN, // TODO: 1) Get it work with httpOnly 2) Get it work on the server. See SignUpForm.tsx // httpOnly: true, diff --git a/src/screens/SignIn/SignInForm/spec.tsx b/src/screens/SignIn/SignInForm/spec.tsx index e3ae70ae..c2dc3b45 100644 --- a/src/screens/SignIn/SignInForm/spec.tsx +++ b/src/screens/SignIn/SignInForm/spec.tsx @@ -29,7 +29,7 @@ describe('SignInForm', () => { }, result: () => { mutationCalled = true; - return { data: { signIn: { sessionToken: '1' } } }; + return { data: { signIn: { token: '1' } } }; }, }, ]; diff --git a/src/screens/SignUp/SignUpForm/index.tsx b/src/screens/SignUp/SignUpForm/index.tsx index 9de7772b..e48f3fd9 100644 --- a/src/screens/SignUp/SignUpForm/index.tsx +++ b/src/screens/SignUp/SignUpForm/index.tsx @@ -82,7 +82,7 @@ const SignUpForm = ({ }, }); - cookie.set('session', data?.signUp.sessionToken || '', { + cookie.set('session', data?.signUp.token || '', { expires: EXPIRES_IN, // TODO: 1) Get it work with httpOnly 2) Get it work on the server. See SignUpForm.tsx // httpOnly: true, diff --git a/src/screens/SignUp/SignUpForm/spec.tsx b/src/screens/SignUp/SignUpForm/spec.tsx index 1dc6da00..6f62c460 100644 --- a/src/screens/SignUp/SignUpForm/spec.tsx +++ b/src/screens/SignUp/SignUpForm/spec.tsx @@ -30,7 +30,7 @@ describe('SignUpForm', () => { }, result: () => { mutationCalled = true; - return { data: { signUp: { sessionToken: '1' } } }; + return { data: { signUp: { token: '1' } } }; }, }, ];