Build GraphQL Authentication Resolvers for Google Auth
We'll continue to update the GraphQL resolver functions we've prepared to allow users to log-in & log-out of our application.
Building the Authentication Resolvers#
We've set up functions needed to interact with Google's Node.js Client to either generate an authentication URL or get user information for a user logging in. We'll now begin to establish our GraphQL schema and the resolvers for the fields our client application can interact with to handle this functionality.
We'll first modify our GraphQL schema and introduce some new type definitions.
We'll want the React client to pass a code
argument to the logIn
mutation to help conduct the login process in our server. Good convention often finds people specifying the arguments of a mutation within an input
object. In the src/graphql/typeDefs.ts
file, we'll create a new input
object type to represent the input that can be passed into the logIn
mutation. We'll label this input
object type LogInInput
and it is to contain a non-null code
property of type GraphQL String
.
input LogInInput {
code: String!
}
We'll also state that the logIn
mutation field is to accept an input
argument of type LogInInput
. Furthermore, we'll have the logIn
mutation expect input
as an optional argument. This is because, in the next couple of lessons, we'll investigate how users will also be able to log-in with the presence of a request cookie.
When the logIn
and logOut
mutations resolve successfully, we'll expect them both to return a GraphQL object type we'll create shortly labeled Viewer
.
import { gql } from "apollo-server-express";
export const typeDefs = gql`
input LogInInput {
code: String!
}
type Query {
authUrl: String!
}
type Mutation {
logIn(input: LogInInput): Viewer!
logOut: Viewer!
}
`;
Viewer GraphQL Object Type#
Viewer
is an object that is to represent the actual person looking/using our app (i.e. the person viewing our app). We'll create an object type to represent what a viewer object is to contain. It will have the following fields and corresponding field types:
id: ID
- a unique identifier since every user in our database is to have a unique id.token: String
- a unique token value to help in countering Cross-Site Request Forgery, with which we'll learn more about in Module 5.avatar: String
- the viewer's avatar image URL.hasWallet: Boolean
- aboolean
value to indicate if the viewer has connected to the payment processor in our app (Stripe).didRequest: Boolean!
- aboolean
value to indicate if a request has been made from the client to obtain viewer information.
All the fields of the Viewer
object type except for the didRequest
field are optional. This is because the person viewing the app could be logged out, or doesn't have an account on our platform. In this case, we won't have any specific viewer information (id
, avatar
, etc.) but we'll still want our client to know that we did attempt to obtain viewer information. This is the reason as to why the didRequest
field is a non-optional Boolean
.
We'll get a better understanding of the purpose of these fields once we start to write more of our implementation. At this moment, the src/graphql/typeDefs.ts
file of our server project will look like the following:
import { gql } from "apollo-server-express";
export const typeDefs = gql`
type Viewer {
id: ID
token: String
avatar: String
hasWallet: Boolean
didRequest: Boolean!
}
input LogInInput {
code: String!
}
type Query {
authUrl: String!
}
type Mutation {
logIn(input: LogInInput): Viewer!
logOut: Viewer!
}
`;
Viewer TypeScript Interface#
We'll now create the corresponding TypeScript definition for a Viewer
object in our TypeScript code. We'll do this in the lib/types.ts
file since the Viewer
TypeScript type we'll create will be accessed in multiple parts of our server application.
The Viewer
interface type we'll create in the lib/types.ts
file will look very similar to the Viewer
object type in our GraphQL schema with some minor differences such as:
In our
Viewer
TypeScript interface, we'll label the identifying field as_id
instead ofid
because we are to reference the same_id
field from our MongoDB database in our TypeScript code. We'll only have the identifying field of theViewer
return asid
in a soon to be created resolver function in theViewer
GraphQL object.In our
Viewer
TypeScript interface, we'll have awalletId
field instead ofhasWallet
becausewalletId
will be an actualid
we'll get from our payment processor (Stripe) and we'll store in our database. We won't need to pass this sensitive information to the client which is why we resolve it to the client as ahasWallet
boolean
field which is to betrue
if the viewer has awalletId
orundefined
if viewer doesn't.
With that said, the Viewer
TypeScript interface we'll create in the lib/types.ts
file will look as follows:
export interface Viewer {
_id?: string;
token?: string;
avatar?: string;
walletId?: string;
didRequest: boolean;
}
Viewer, id, and hasWallet resolvers#
The _id
and walletId
conversion to id
and hasWallet
in our GraphQL schema will be done by creating resolver functions for the Viewer
GraphQL object. If we recall, trivial resolvers often don't need to be declared when we simply attempt to return a value from an object argument using the same key specified in the object type (e.g. id
-> viewer.id
). In this case, however, we'll need to declare the resolver functions for the id
and hasWallet
fields for our GraphQL Viewer
object type since we want to resolve these different fields.
We'll add the id
and hasWallet
resolver functions to a Viewer
object we'll create in the viewerResolvers
map within src/graphql/resolvers/Viewer/index.ts
.
import { IResolvers } from "apollo-server-express";
export const viewerResolver: IResolvers = {
Query: {
// ...
},
Mutation: {
// ...
},
Viewer: {
id: () => {},
hasWallet: () => {}
}
};
Our id
resolver function will take a viewer
input argument and return the value of viewer._id
. The viewer
input argument will have the shape of the Viewer
interface we've set up in the lib/types.ts
so we'll import the Viewer
interface and set it as the type of the viewer
argument.
import { IResolvers } from "apollo-server-express";
import { Viewer } from "../../../lib/types";
export const viewerResolver: IResolvers = {
Query: {
// ...
},
Mutation: {
// ...
},
Viewer: {
id: (viewer: Viewer): string | undefined => {
return viewer._id;
},
hasWallet: () => {}
}
};
Where is this
viewer
input argument coming from? The first positional argument of resolver functions will always be the root object returned from the parent fields. In this example, the parentlogIn
andlogOut
mutations will return the viewer object when resolved, and the resolver functions we define in theViewer
receives this viewer object and maps the data as we expect in our GraphQL schema.
The Viewer hasWallet
resolver function will receive the viewer
input argument and return true
if the viewer walletId
exists. Otherwise, we'll have it return undefined
.
import { IResolvers } from "apollo-server-express";
import { Viewer } from "../../../lib/types";
export const viewerResolver: IResolvers = {
Query: {
// ...
},
Mutation: {
// ...
},
Viewer: {
id: (viewer: Viewer): string | undefined => {
return viewer._id;
},
hasWallet: (viewer: Viewer): boolean | undefined => {
return viewer.walletId ? true : undefined;
}
}
};
We could also have the
hasWallet()
resolver function just returnboolean
values oftrue
orfalse
.
Query.authUrl
#
With the Viewer
object defined in our TypeScript code and GraphQL schema, we'll now modify the authUrl()
query resolver function to return the authUrl
from the Google
object we've created in our lib/api/
folder.
In the src/graphql/resolvers/Viewer/index.ts
file, we'll import the Google
object. In the authUrl()
resolver function of the Query
object, we'll use a try...catch
pattern to have the field simply return the authUrl
field of our Google
API object or throw an error.
import { IResolvers } from "apollo-server-express";
import { Google } from "../../../lib/api";
export const viewerResolver: IResolvers = {
Query: {
authUrl: (): string => {
try {
return Google.authUrl;
} catch (error) {
throw new Error(`Failed to query Google Auth Url: ${error}`);
}
}
},
Mutation: {
// ...
},
Viewer: {
// ...
}
};
Mutation.logIn
#
We'll now modify the resolver function for the root level logIn
mutation. The logIn
mutation is to expect an input
that contains a code
property. We'll define a TypeScript interface type to describe the shape of the arguments the logIn
mutation is to expect. We'll define this interface in a types.ts
we'll keep in the src/graphql/resolvers/Viewer/
folder.
server/
src/
graphql/
resolvers/
Viewer/
index.ts
types.ts
// ...
// ...
We'll name the interface to describe the shape of arguments the logIn
mutation is to accept LogInArgs
and it will have an input
property that is to be an object that has a code
of type string
. Since this input
property is optional, we'll say the input object might also be null
.
export interface LogInArgs {
input: { code: string } | null;
}
We'll head back to the viewerResolvers
map and look to build the implementation for the logIn()
resolver function.
This page is a preview of TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two