Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Microsoft Azure profile imageAaron Powell
Aaron Powell forMicrosoft Azure

Posted on • Originally published ataaron-powell.com on

     

GraphQL on Azure: Part 7 - Server-side Authentication

In our journey into GraphQL on Azure we’ve only created endpoints that can be accessed by anyone. In this post we’ll look at how we can add authentication to our GraphQL server.

For the post, we’ll use theApollo Server andAzure Static Web Apps for hosting the API, mainly because SWAprovides security (and if you’re wondering, this is how I came across the need to writethis last post).

If you’re new to GraphQL on Azure, I’d encourage you to check outpart 3 in which I go over how we can create a GraphQL server using Apollo and deploy that to an Azure Function, which is the process we’ll be using for this post.

Creating an application

The application we’re going to use today is a basic blog application, in which someone can authenticate against, create a new post with markdown and before saving it (it’ll just use an in-memory store). People can then comment on a post, but only if they are logged in.

Let’s start by defining set of types for our schema:

typeComment{id:ID!comment:String!author:Author!}typePost{id:ID!title:String!body:String!author:Author!comments:[Comment!]!comment(id:ID!):Comment}typeAuthor{id:ID!userId:String!name:String!email:String}
Enter fullscreen modeExit fullscreen mode

We'll add some queries and mutations, along with the appropriate input types:

typeQuery{getPost(id:ID!):PostgetAllPosts(count:Int!=5):[Post!]!getAuthor(userId:String!):Author}inputCreatePostInput{title:String!body:String!authorId:ID!}inputCreateAuthorInput{name:String!email:StringuserId:String!}inputCreateCommentInput{postId:ID!authorId:ID!comment:String!}typeMutations{createPost(input:CreatePostInput!):Post!createAuthor(input:CreateAuthorInput!):Author!createComment(input:CreateCommentInput!):Post!}schema{query:Querymutation:Mutations}
Enter fullscreen modeExit fullscreen mode

And now we have our schema ready to use. So let’s talk about authentication.

Authentication in GraphQL

Authentication in GraphQL is an interesting problem, as the language doesn’t provide anything for it, but instead relies on the server to provide the authentication and for you to work out how that is applied to the queries and mutations that schema defines.

Apollo providessome guidance on authentication, through the use of acontext function, that has access to the incoming request. We can use this function to unpack the SWA authentication information and add it to thecontext object. To get some help here, we’ll use the@aaronpowell/static-web-apps-api-auth library, as it can tell us if someone is logged in and unpack the client principal from the header.

Let’s implement acontext function to add the authentication information from the request (for this post, I’m going to skip over some of the building blocks and implementation details, such as how resolvers work, but you can find them in the complete sample at the end):

constserver=newApolloServer({typeDefs,resolvers,context:({request}:{request:HttpRequest})=>{return{isAuthenticated:isAuthenticated(request),user:getUserInfo(request)};}});
Enter fullscreen modeExit fullscreen mode

Here we’re using the npm package to set theisAuthenticated anduser properties of the context, which works by unpacking theSWA authentication information from the header (you don’tneed my npm package, it’s just helpful).

Applying Authentication with custom directives

Thiscontext object will be available in all resolvers, so we can check if someone is authenticated and the user info, if required. So now that that’s available, how do we apply the authentication rules to our schema? It would make sense to have something at a schema level to handle this, rather than a set of inline checks within the resolvers, as then it’s clear to someone reading our schema what the rules are.

GraphQL Directives are the answer. Directives are a way to add custom behaviour to GraphQL queries and mutations. They’re defined in the schema, and can be applied to a type, field, argument or query/mutation.

Let’s start by defining a directive that, when applied somewhere, requires a user to be authenticated:

directive@isAuthenticatedonOBJECT|FIELD_DEFINITION
Enter fullscreen modeExit fullscreen mode

This directive will be applied to any type, field or argument, and will only be applied if theisAuthenticated property of the context istrue. So, where shall we use it? The logical first place is on all mutations that happen, so let’s update the mutation section of the schema:

typeMutations@isAuthenticated{createPost(input:CreatePostInput!):Post!createAuthor(input:CreateAuthorInput!):Author!createComment(input:CreateCommentInput!):Post!}
Enter fullscreen modeExit fullscreen mode

We’ve now added@isAuthenticated to theMutationsObject Type in the schema. We could have added it to each of theField Definitions, but it’s easier to just add it to theMutationsObject Type, want it on all mutations. Right now, we don’t have any query that would require authentication, so let’s just stuck with the mutation.

Implementing a custom directive

Defining the Directive in the schema only tells GraphQL that this is athing that the server can do, but it doesn’t actually do anything. We need to implement it somehow, and we do that in Apollo by creating a class that inherits fromSchemaDirectiveVisitor.

import{SchemaDirectiveVisitor}from"apollo-server-azure-functions";exportclassIsAuthenticatedDirectiveextendsSchemaDirectiveVisitor{}
Enter fullscreen modeExit fullscreen mode

As this directive can support either Object Types or Field Definitions we've got two methods that we need to implement:

import{SchemaDirectiveVisitor}from"apollo-server-azure-functions";exportclassIsAuthenticatedDirectiveextendsSchemaDirectiveVisitor{visitObject(type:GraphQLObjectType){}visitFieldDefinition(field:GraphQLField<any,any>,details:{objectType:GraphQLObjectType;}){}}
Enter fullscreen modeExit fullscreen mode

To implement these methods, we're going to need to override theresolve function of the fields, whether it's all fields of the Object Type, or a single field. To do this we'll create a common function that will be called:

import{SchemaDirectiveVisitor}from"apollo-server-azure-functions";exportclassIsAuthenticatedDirectiveextendsSchemaDirectiveVisitor{visitObject(type:GraphQLObjectType){this.ensureFieldsWrapped(type);type._authRequired=true;}visitFieldDefinition(field:GraphQLField<any,any>,details:{objectType:GraphQLObjectType;}){this.ensureFieldsWrapped(details.objectType);field._authRequired=true;}ensureFieldsWrapped(objectType:GraphQLObjectType){}}
Enter fullscreen modeExit fullscreen mode

You'll notice that we always pass in aGraphQLObjectType (either the argument or unpacking it from the field details), and that's so we can normalise the wrapper function for all the things we need to handle. We're also adding a_authRequired property to the field definition or object type, so we can check if authentication is required.

Note: If you're using TypeScript, as I am in this codebase, you'll need to extend the type definitions to have the new fields as follows:

import{GraphQLObjectType,GraphQLField}from"graphql";declaremodule"graphql"{classGraphQLObjectType{_authRequired:boolean;_authRequiredWrapped:boolean;}classGraphQLField<TSource,TContext,TArgs={[key:string]:any}>{_authRequired:boolean;}}
Enter fullscreen modeExit fullscreen mode

It's time to implementensureFieldsWrapped:

ensureFieldsWrapped(objectType:GraphQLObjectType){if(objectType._authRequiredWrapped){return;}objectType._authRequiredWrapped=true;constfields=objectType.getFields();for(constfieldNameofObject.keys(fields)){constfield=fields[fieldName];const{resolve=defaultFieldResolver}=field;field.resolve=isAuthenticatedResolver(field,objectType,resolve);}}
Enter fullscreen modeExit fullscreen mode

We're going to first check if the directive has been applied to this object already or not, since the directive might be applied multiple times, we don't need to wrap what's already wrapped.

Next, we'll get all the fields off the Object Type, loop over them, grab theirresolve function (if defined, otherwise we'll use the default GraphQL field resolver) and then wrap that function with ourisAuthenticatedResolver function.

constisAuthenticatedResolver=(field:GraphQLField<any,any>,objectType:GraphQLObjectType,resolve:typeofdefaultFieldResolver):typeofdefaultFieldResolver=>(...args)=>{constauthRequired=field._authRequired||objectType._authRequired;if(!authRequired){returnresolve.apply(this,args);}constcontext=args[2];if(!context.isAuthenticated){thrownewAuthenticationError("Operation requires an authenticated user");}returnresolve.apply(this,args);};
Enter fullscreen modeExit fullscreen mode

This is kind of like partial application, but in JavaScript, we're creating a function that takes some arguments and in turn returns a new function that will be used at runtime. We're going to pass in the field definition, the object type, and the originalresolve function, as we'll need those at runtime, so this captures them in the closure scope for us.

For the resolver, it is going to look to see if the field or object type required authentication, if not, return the result of the original resolver.

If it did, we'll grab thecontext (which is the 3rd argument to an Apollo resolver), check if the user is authenticated, and if not, throw anAuthenticationError, which is provided by Apollo, and if they are authenticated, we'll return the original resolvers result.

Using the directive

We've added the directive to our schema, created an implementation of what to do with that directive, all that's left is to tell Apollo to use it.

For this, we'll update theApolloServer in ourindex.ts file:

constserver=newApolloServer({typeDefs,resolvers,context:({request}:{request:HttpRequest})=>{return{isAuthenticated:isAuthenticated(request),user:getUserInfo(request)};},schemaDirectives:{isAuthenticated:IsAuthenticatedDirective}});
Enter fullscreen modeExit fullscreen mode

TheschemaDirectives property is where we'll tell Apollo to use our directive. It's a key/value pair, where the key is the directive name, and the value is the implementation.

Conclusion

And we're done! This is a pretty simple example of how we can add authentication to a GraphQL server using a custom directive that uses the authentication model of Static Web Apps.

We saw that using a custom directive allows us to mark up the schema, indicating, at a schema level, which fields and types require authentication, and then have the directive take care of the heavy lifting for us.

You can find the full sample application, including a React UIon my GitHub, and the deployed appis here, but remember, it's an in-memory store so the data is highly transient.

Azure Static Website React Template

This repository contains a template for creating anAzure Static Web App projects using React + TypeScript.

In the template there isCreate React App site using TypeScript and anapi folder with an emptyAzure Functions, also using TypeScript.

To get started, click theUse this template button to create a repository from this template, and check out theGitHub docs on using templates.

Running The Application

From a terminal runnpm start from both the repository root andapi folder to start the two servers, the web application will be onhttp://localhost:3000 and the API onhttp://localhost:7071. Alternatively, you can use the VS Code launch ofRun full stack to run both together with debuggers attached.






Bonus - restricting data to the current user

If we look at theAuthor type, there's some fields available that we might want to restrict to just the current user, such as their email or ID. Let's create anisSelf directive that can handle this for us.

directive@isSelfonOBJECT|FIELD_DEFINITIONtypeAuthor{id:ID!@isSelfuserId:String!@isSelfname:String!email:String@isSelf}
Enter fullscreen modeExit fullscreen mode

With this we're saying that theAuthor.name field is available to anyone, but everything else about their profile is restricted to just them. Now we can implement that directive:

import{UserInfo}from"@aaronpowell/static-web-apps-api-auth";import{AuthenticationError,SchemaDirectiveVisitor}from"apollo-server-azure-functions";import{GraphQLObjectType,defaultFieldResolver,GraphQLField}from"graphql";import{Author}from"../generated";import"./typeExtensions";constisSelfResolver=(field:GraphQLField<any,any>,objectType:GraphQLObjectType,resolve:typeofdefaultFieldResolver):typeofdefaultFieldResolver=>(...args)=>{constselfRequired=field._isSelfRequired||objectType._isSelfRequired;if(!selfRequired){returnresolve.apply(this,args);}constcontext=args[2];if(!context.isAuthenticated||!context.user){thrownewAuthenticationError("Operation requires an authenticated user");}constauthor=args[0]asAuthor;constuser:UserInfo=context.user;if(author.userId!==user.userId){thrownewAuthenticationError("Cannot access data across user boundaries");}returnresolve.apply(this,args);};exportclassIsSelfDirectiveextendsSchemaDirectiveVisitor{visitObject(type:GraphQLObjectType){this.ensureFieldsWrapped(type);type._isSelfRequired=true;}visitFieldDefinition(field:GraphQLField<any,any>,details:{objectType:GraphQLObjectType;}){this.ensureFieldsWrapped(details.objectType);field._isSelfRequired=true;}ensureFieldsWrapped(objectType:GraphQLObjectType){if(objectType._isSelfRequiredWrapped){return;}objectType._isSelfRequiredWrapped=true;constfields=objectType.getFields();for(constfieldNameofObject.keys(fields)){constfield=fields[fieldName];const{resolve=defaultFieldResolver}=field;field.resolve=isSelfResolver(field,objectType,resolve);}}}
Enter fullscreen modeExit fullscreen mode

This directive does take an assumption on how it's being used, as it assumes that the first argument to theresolve function is anAuthor type, meaning it's trying to resolve the Author through a query or mutation return, but otherwise it works very similar to theisAuthenticated directive, it ensures someone is logged in, and if they are, it ensures that the current user is the Author requested, if not, it'll raise an error.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Invent with purpose

Any language. Any platform.

More fromMicrosoft Azure

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp