Blog

How to engage GraphQL, .Net and React together: Part II. Veda Versum backend authentication.

Mikhail Shabanov
Mikhail Shabanov
February 21st, 2022

 

“If you optimize everything, you will always be unhappy” –  Donald Knuth

Having a good tool allows you to complete tasks effectively and enjoy the process. Twice the fun to develop the tool yourself 🙂

Hi, It’s Mikhail from Centigrade again. And let’s continue to build our tool – knowledge base called Veda Versum. This is my second article in the series “How to engage GraphQL, .Net and React together”. In the Previous article we have defined our target application, requirements and architecture. We have also chosen the .Net 6 for backend with hot chocolate library for GraphQL API and React as the UI framework. And we have created the backend application scaffold. Now it’s time to give life to our backend.

Today we will add Authentication to backend. We will define and implement the data persistence for our application. We will talk about GraphQL resolvers and data loaders and will say a couple of words about testing.

Let’s get it started! 😉

1.   Authentication and Authorization

We have already discussed in the previous article that each user can open application, create some knowledge cards, read cards and see other users who are being online. Sure, our Veda Versum application is supposed to know each user personally :-). As you already know, the common way to “show yourself” in the IT world is  to log into the system. Yep, to do this you should have an account with login and password. Nothing new. We have to provide the same functionality for our brand new application. But we will not reinvent the wheel and will not implement the process of keeping users credentials by our own. We will use the standard protocol called OAuth 2. According to this standard we are assumed to use already existing authentication server for our users. A lot of providers like Microsoft, Google, Twitter, Facebook and others support this protocol. So if you want to implement your own user management but don’t want to run your own authentication server, you can use those. And one significant player in the field is Auth0 which specializes on authentication services. But as far as our team uses GitLab for most of our projects, all of us have an account in GitLab. Therefore GitLab.com will be our OAuth provider. So Veda Versum user doesn’t have to create one more account to login in Veda Versum, he or she can use already existing account in GitLab. Our application will redirect user to GitLab login page and after user enters his/her login and password, he or she will be redirected back to our application but with some authentication token. And our application will operate only with this token.

As far as our application has Frontend, Backend, and GitLab as OAuth 2 provider, authentication schema will look like this:

Authentication scheme

Fig. 1: Authentication scheme

The whole scheme looks a bit complicated. I will try to explain all the details and guide you through all processes step by step.

1.1 GitLab Authentication mutation

First and foremost, we have to set up OAuth in the GitLab settings page. Follow these instructions to create Group Owned Application. As a result we will have Application Id and a secret. We will need them for next steps.

This is the link to Gitlab OAuth API to implement the authentication. To implement steps 2 and 4 from Figure 1 we need to call oauth/authorize GitLab API method. This method will be called from UI. Later to implement steps 6 and 7 we must call method oauth/token. And for steps 8 and 9 – to call method /api/v4/user.

Frontend is not our goal for today, here we will focus on the backend part. To implement steps from 5 to 10 from Figure 1 we must create a brand-new mutation in our GraphQL API which should retrieve GiltLabUser information. It will take GitLab single-use Auth Code as a parameter and return secured JWT token containing the user information. This mutation will incapsulate the implementation of steps 6 – 9 from Figure 1.

To do that we have to change our project this way:

  • Define 4 parameters in json;
  • Create class GitLabOauthSettings which will read and hold values of these 4 parameters;
  • Define IGitlabOauthService interface with 2 methods in it;
  • Define GitLabOauthService class which implements interface and takes Oauth app settings and GitLab http client as dependency injection objects;
  • Define new mutation with GitLabAuthenticate This mutation will use IGitlabOauthService as dependency injection object;
  • Set up configuration, http client, GitLabOauthService and mutation in cs.

The code base should look like this:

GitLab authentication mutation class diagram

Fig. 2: GitLab authentication mutation class diagram

So, as Linus Torvalds said: “Talk is cheap, show me the code”.

 

appsettings.json:
  "GitLabOauth": {
    "BaseAddress": "https://gitlab.example.com",
    "ClientId": "FakeClientId",
    "Secret": "FakeSecret",
    "JwtSecret": "FakeJwtSecret"
  },

One tip regarding app settings. I’m using app secrets mechanism to keep sensitive data in the development machine instead of appsettings.json. Having this I’m always debugging the code with real sensitive data and at the same time I will never commit them to the public repository.

OAuthMutation.cs:
        /// <summary>
        /// This method accepts GitLab oauth code, and returns JWT token with GutLab user as claim
        /// </summary>
        /// <param name="oauthCode">OAuth code can be generated by this URL https://gitlab.example.com/oauth/authorize</param>
        /// <remarks>
        /// More info about GitLab OAuth https://docs.gitlab.com/ee/api/oauth2.html
        /// </remarks>
        public async Task<string> GitLabAuthenticate(string oauthCode)
        {
            try
            {
                var user = await _oauthService.GetUser(oauthCode);
                if (user == null)
                {
                    throw new ApplicationException($"Can not find GitLab user by code {oauthCode}");
                }
                return _oauthService.GenerateToken(user);
            }
            catch (Exception e)
            {
                _logger.LogError(e,"Authentication fails");
                throw;
            }
        }

You can find the whole mutation class in the repository. Here is the mutation method which is just calling two service methods – get user and generate JWT token

GitLabOauthService.cs

        /// <inheritdoc />
        public string GenerateToken(User user)
        {
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.JwtSecret));
            var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            var serializedUser = JsonSerializer.Serialize(user);

            // generate jwt token
            var claims = new[]
            {
                new Claim(ClaimTypes.Name, user.Name?? "unknown"),
                new Claim(ClaimTypes.Email, user.Email?? "unknown"),
                new Claim(ClaimTypes.UserData, serializedUser)
            };

            var token = new JwtSecurityToken(
                issuer: JwtIssuer,
                audience: JwtIssuer,
                claims: claims,
                expires: DateTime.Now.AddDays(30), // Token is valid for 1 month. Don't use this parameter in production!
                signingCredentials: credentials);

            return new JwtSecurityTokenHandler().WriteToken(token);
        }

        /// <inheritdoc />
        public async Task<User?> GetUser(string oAuthCode)
        {
            var token = await GetAccessToken(oAuthCode);
            return await GetGitLabUser(token);
        }

        private async Task<string> GetAccessToken(string oAuthCode)
        {
            var parameters = new Dictionary<string, string>
            {
                { "client_id", _settings.ClientId }, 
                { "client_secret", _settings.Secret },
                { "code", oAuthCode },
                { "grant_type", "authorization_code" },
                { "redirect_uri", "https://localhost:5001" }
            };
            var encodedContent = new FormUrlEncodedContent(parameters!);

            var client = _httpClientFactory.CreateClient(GitLabHttpClientName);

            var url = $"{_settings.BaseAddress}/oauth/token";

            var response = await client.PostAsync(url, encodedContent).ConfigureAwait(false);
            response.EnsureSuccessStatusCode();

            await using var responseStream = await response.Content.ReadAsStreamAsync();
            var authTokenResponse = await JsonSerializer.DeserializeAsync<OAuthTokenResponse?>(responseStream);

            if(string.IsNullOrEmpty(authTokenResponse?.AccessToken))
            {
                throw new ApplicationException($"Can not get access_token from url '{_settings.BaseAddress}' and Auth code '{oAuthCode}'");
            }
            return authTokenResponse.AccessToken;
        }

        private async Task<User?> GetGitLabUser(string token)
        {
            string url = $"{_settings.BaseAddress}/api/v4/user";

            var client = _httpClientFactory.CreateClient(GitLabHttpClientName);
            client.DefaultRequestHeaders.Authorization
                = new AuthenticationHeaderValue("Bearer", token);
            client.DefaultRequestHeaders.Add("User-Agent", "VedaVersum");

            var response = await client.GetAsync(url);

            response.EnsureSuccessStatusCode();
            
            await using var responseStream = await response.Content.ReadAsStreamAsync();
            return await JsonSerializer.DeserializeAsync<User?>(responseStream);
        }

You can find the whole service class in the repository. Here are three main methods:

The first method calls GitLab API to get access token. We will need this token in the second method – to get GitLab user from GitLab API. And the third method generates JWT token with user information secured by SHA algorithm with some secret key stored in application setting.

startup.cs:

        public void ConfigureServices(IServiceCollection services)
        {
            // GitLab authorization configuration
            var gitLabOauthConfig = new GitLabOauthSettings();
            Configuration.GetSection("GitLabOauth").Bind(gitLabOauthConfig);
            services.AddSingleton(gitLabOauthConfig);

            // GitLab authorization
            services.AddHttpClient(GitLabOauthService.GitLabHttpClientName);
            services.AddTransient<IGitLabOauthService, GitLabOauthService>();
            services
                .AddGraphQLServer()
                .AddInMemorySubscriptions()
                .AddQueryType<VedaVersumQuery>()
                    .AddType<VedaVersumCardObjectType>()
                .AddMutationType(d => d.Name("Mutation"))
                    .AddType<OAuthMutation>() // Register new OAuth Mutation in the GraphQL API
                    .AddType<VedaVersumMutation>()
                .AddSubscriptionType<VedaVersumSubscription>();
       }

Here we are reading configuration from app settings and adding settings to DI container. We push new GitLabOauthService to DI container and register new OAuthMutation in the GraphQL API

Now our service is ready to generate JWT tokens. If you take a look to Figure 1, you will notice that we still need to get GitLab single-use Authentication code somewhere. These are steps 2, 3 and 4 to be managed by Frontend in the future. But as far as we don’t have frontend yet, we will do these steps manually. We should open the browser and send this url to GitLab:

https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=https://localhost:5001&response_type=code&state=TEMPORARY_STATE&scope=read_user

You should replace 3 parameters:

  • gitlab.example.com to your gitlab server URL
  • APP_ID to your own Application Id which you received when creating GitLab group owned application
  • TEMPORARY_STATE can be any string. I’m using GUID generator to generate it.

When you navigate to this url, you will be redirected to GitLab authentication page. And after successful login, you will be redirected to https://localhost:5001 which does not exist yet. All you need at the moment will be in the browser address line:

https://localhost:5001/?code=4debdf62d4458dbd02be661661506c869582232b0bf7632d5e10e74045318eb0&state=29aa0c42-58d0-4129-9d76-33094fa56401

We will use this code in our Authentication mutation. When you start VedaVersum application, there will be shown GraphQL BananaCakePop IDE, where you can call the mutation:

Authentication mutation execution
Fig. 3: Authentication mutation execution

If you did everything right, the mutation will return you the JWT token back. You can test this token using https://jwt.io/

 

JWT token
Figure 4. JWT token

That means we have the right token. In the future this token will be stored somewhere in the Frontend. And each time frontend calls queries or mutations in our GraphQL API, it should add this token as http header to each request to authenticate the user. But as far as we don’t have frontend yet, let’s keep this token somewhere in notepad. We will need it in the next chapter ?

1.2 Authorization by JWT token

We have already created mechanism to authenticate user and store user information as encrypted JWT token at Frontend. So far, so good ? Now it is time to secure our GraphQL API and allow only the users with valid JWT tokens to use its queries and mutations.

Each http request in Asp.net core server goes through some “pipeline”. This pipeline has its own context. And developers can inject different “middleware” types into this pipeline and manipulate the context. Asp net core has standard authorization mechanisms which are implemented as middleware too. We have also added GraphQL server by ChilliCream as middleware, which has its own context per each request.

Asp.net core pipeline
Figure 5. Asp.net core pipeline

Firstly we need to inject our JWT authorization logic into standard Authentication middleware. Secondly we need to add User to GraphQL middleware context.

To add JWT Token authentication as standard Asp.net Core middleware we need to add nuget packages “Microsoft.AspNetCore.Authentication.JwtBearer” and “System.IdentityModel.Tokens.Jwt” to our project. And then set up Authentication service in the DI Container like this:

            // Token validation
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        ValidIssuer = GitLabOauthService.JwtIssuer,
                        ValidAudience = GitLabOauthService.JwtIssuer,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(gitLabOauthConfig.JwtSecret))
                    };
                });

Add Authentication and Authorization middleware in the Configure method:

            app
                .UseWebSockets()
                .UseRouting()
                .UseAuthentication() // Authentication middleware
                .UseAuthorization() // Authorization middleware
                .UseEndpoints(endpoints =>
                {
                    endpoints.MapGraphQL();
                });

Well, we have just implemented blocks number 1 and 2 from Figure 5.

To add Authorization into GraphQL Server middleware, we have to add new nuget package “HotChocolate.AspNetCore.Authorization”. And to add authorization to the GraphQL DI setup in startup.cs:

        services
            .AddGraphQLServer()
	…
            .AddAuthorization()

After that we have to add [Authorize] attribute to the VedaVersumQuery, VedaVersumMutation and VedaVersumSubscription classes:

Add [Authorize] attribute to the VedaVersumQuery

Here we have implemented block number 3 from Figure 5.

Now we can try to call any query or mutation without any authorization tokens:

Authentication error for anonymous users
Fig. 6: Authentication error for anonymous users

As you can see, anonymous users cannot use our GraphQL API anymore.

But if we add the http header {“Authorization”: “Bearer …”} with valid token to our request, mutation is executed as expected:

Normal mutation execution for authorized users
Fig. 7: Normal mutation execution for authorized users

The last but not the least: if user is authorized, User object of PrincipalIdentity type will be added to the asp.net pipeline context. And this object will be accessible in all the middlewares after “Authorization” middleware, including GraphQL Resolver at the block 4 (Figure 5). But in the Business Logic we need to operate the GitLabUser object with all the requested properties. We can convert that PrincipalIdentity into GitLabUser right in the resolver. But if we have a lot of resolvers, we have to make that conversion each time. It would be better to convert PrincipalIdentity into GitLabUser earlier in the pipeline and put converted GitLabUser object into the pipeline’s context. And here is the example how we can do this in the level of block 3 (from Figure 5):

        services
            .AddGraphQLServer()
	…
            .AddAuthorization()
            .AddHttpRequestInterceptor(
                (context, executor, builder, ct) =>
                {
                    // Deserializing GitLab user from JWT token data
                    if(context.User != null)
                    {
                        var serializedUser = context.User.Claims.Where(c => c.Type == ClaimTypes.UserData)
                            .Select(c => c.Value).SingleOrDefault();
                        if(!string.IsNullOrEmpty(serializedUser))
                        {
                            var user = JsonSerializer.Deserialize<User>(serializedUser);
                            builder.SetProperty("GitLabUser", user);
                        }
                    }
                    return ValueTask.CompletedTask;
                })
            .ModifyRequestOptions(opt => opt.IncludeExceptionDetails = true);

This interceptor gets GitlabUser from PrincipalIdentity and put this object into context using key “GitLabUser”. Later we can use this object in any of our resolvers like this:

interceptor gets GitlabUser from PrincipalIdentity

OK, we have added the full round of authentication and authorization to our backend using GitLab as OAuth 2 authentication provider. We have also made our GraphQL API fully secured.

The last but not the least. We have created one of the most complicated parts of the program and the only way to test its capability is to  run the application and do all the necessary steps manually. But sometimes we need to check fast if everything works. And it is even better to test after each commit if we haven’t broken anything. I have created the test project which uses NUnit testing framework and Moq library for creating mock objects. There is a test scenario where I’m mocking the .Net HTTP client class and checking if Gitlab Web API methods are called with appropriate URIs, parameters and authorization tokens. You can check out the code in the GitHub repository. We will talk about unit tests in details in our next article.

That’s all for today :-). In the next article we will dive into the data persistency and GraphQL resolvers and Data loaders. See you! 🙂

Want to know more about our services, products or our UX process?
We are looking forward to hearing from you.

Senior UX Manager
+49 681 959 3110

Before sending your request, please confirm that we may contact you by clicking in the checkbox above.