# .NET OAuth2 Guide

.NET is a software framework developed by Microsoft that is used for building a wide range of applications, including web applications, mobile apps, desktop applications, cloud services, and more.

However, auth with .NET can be overly complex and difficult to set up. That’s where PropelAuth comes in. This guide will help you install PropelAuth in your .NET application using our [OAuth2 Support](https://docs.propelauth.com/overview/authentication/oauth2).

Only wanting to use PropelAuth for your .NET backend? Check out our [.NET reference docs](https://docs.propelauth.com/reference/backend-apis/dot-net) instead

## [Installation](https://docs.propelauth.com/getting-started/additional-framework-guides/dot-net-oauth-guide#installation)

```shell
dotnet add package PropelAuth
```

## [Configuration](https://docs.propelauth.com/getting-started/additional-framework-guides/dot-net-oauth-guide#configuration)

Let’s start by navigating to our OAuth Configuration page. Head over to the Frontend Integration page, click on Advanced Settings followed by Edit OAuth Config.

Here, we can generate a **Client ID** and **Client Secret** (we'll be using these in the next step). We can also set a **Redirect URI**. In this example, we’ll be using the `/auth/callback` path so if you’re testing in localhost, you can enter [http://localhost:8000/auth/callback](http://localhost:8000/auth/callback), for example.

Next, navigate to the **Backend Integration** page of the PropelAuth Dashboard and copy your **Auth URL** and **Public Verifier Key**. These values will be used to validate [access tokens](https://docs.propelauth.com/recipes/access-tokens) generated by your frontend. Paste these values along with the **Client ID** and **Client Secret** into your .NET project.

### Program.cs

```csharp
var AUTH_URL = "https://auth.example.com";
var PUBLIC_KEY = @"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1DsxqIjXqM0i5PL6kFVa\n280S3gl96n2YlO6l9ss2XD/GOoDM11LxnwlIBWFXeRGhOVi4dp2pefY4Bh2rg4Z8\n/Nq1J..\n-----END PUBLIC KEY-----\n";
var CLIENT_ID = "09663a8...";
var CLIENT_SECRET = "f12ac317dd9ec91...";
```

We'll be using the [System.Security.Cryptography Namespace](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography?view=net-9.0) to import the **Public Verifier Key**.

### Program.cs

```csharp
using System.Security.Cryptography;

var rsa = RSA.Create();
rsa.ImportFromPem(PUBLIC_KEY);
```

The next step is to configure our app to use the PropelAuth OAuth2 client as well as JWT authentication. The OAuth2 client will be responsible for handling the user's login flow and retrieving the user's access and refresh tokens. On the other hand, JWT authentication will allow us to validate the access tokens retrieved from the OAuth2 client and retrieve user information from them.

### Program.cs

```csharp
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

builder.Services.AddAuthentication(options =>
{
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = "PropelAuth";
}).AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateAudience = false,
        ValidAlgorithms = new List<string>() { "RS256" },
        ValidIssuer = AUTH_URL,
        IssuerSigningKey = new RsaSecurityKey(rsa),
        ValidateLifetime = true,
        ClockSkew = TimeSpan.Zero
    };
    options.Events = new JwtBearerEvents
    {
        OnMessageReceived = context =>
        {
            string? token = context.Request.Cookies["__pa_at"];
            if (string.IsNullOrEmpty(token))
            {
                return Task.CompletedTask;
            }
            context.Token = token;
            return Task.CompletedTask;
        }
    };
}).AddOAuth("PropelAuth", options =>
{
    options.AuthorizationEndpoint = AUTH_URL + "/propelauth/oauth/authorize";
    options.CallbackPath = "/auth/callback";
    options.ClientId = CLIENT_ID;
    options.ClientSecret = CLIENT_SECRET;
    options.TokenEndpoint = AUTH_URL + "/propelauth/oauth/token";
    options.UserInformationEndpoint = AUTH_URL + "/propelauth/oauth/userinfo";
    options.SaveTokens = true;
    options.Events = new OAuthEvents
    {
        // After OAuth2 has authenticated the user
        OnCreatingTicket = context =>
        {
            if (!string.IsNullOrEmpty(context.AccessToken))
            {
                context.HttpContext.Response.Cookies.Append("__pa_at", context.AccessToken, new CookieOptions()
                {
                    HttpOnly = true,
                    SameSite = SameSiteMode.Lax,
                    IsEssential = true,
                    Secure = true,
                    MaxAge = TimeSpan.FromMinutes(25),
                });
            }
            if (!string.IsNullOrEmpty(context.RefreshToken))
            {
                context.HttpContext.Response.Cookies.Append("__pa_rt", context.RefreshToken, new CookieOptions()
                {
                    HttpOnly = true,
                    SameSite = SameSiteMode.Lax,
                    IsEssential = true,
                    Secure = true,
                    MaxAge = TimeSpan.FromDays(14),
                });
            }
            return Task.CompletedTask;
        }
    };
}).AddCookie();
```

## [Refreshing Access Tokens](https://docs.propelauth.com/getting-started/additional-framework-guides/dot-net-oauth-guide#refreshing-access-tokens)

By default, the `Microsoft.AspNetCore.Authentication.OAuth` Namespace will _not_ automatically refresh access tokens for you. However, you can install middleware to check if the user's access token (stored as a `__pa_at` cookie above) is valid or expired. If expired or issued more than 10 minutes ago, it will automatically refresh the access token using the refresh token.

### TokenRefreshMiddleware.cs

```csharp
using System.IdentityModel.Tokens.Jwt;
using System.Text.Json;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Authentication.JwtBearer;

public class TokenRefreshMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IConfiguration _configuration;
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly TokenValidationParameters _tokenValidationParameters;

public TokenRefreshMiddleware(
        RequestDelegate next,
        IConfiguration configuration,
        IHttpClientFactory httpClientFactory,
        IOptionsMonitor<JwtBearerOptions> jwtOptions)
    {
        _next = next;
        _configuration = configuration;
        _httpClientFactory = httpClientFactory;

// Use the same validation parameters as your JWT Bearer authentication
        _tokenValidationParameters = jwtOptions.Get(JwtBearerDefaults.AuthenticationScheme).TokenValidationParameters;
    }

public async Task InvokeAsync(HttpContext context)
    {
        // Try to get the access and refresh tokens from the cookies
        var accessToken = context.Request.Cookies["__pa_at"];
        var refreshToken = context.Request.Cookies["__pa_rt"];

// If we don't have a refresh token, continue processing the request
        if (string.IsNullOrEmpty(refreshToken))
        {
            await _next(context);
            return;
        }

// If no access token, try to refresh
        if (string.IsNullOrEmpty(accessToken))
        {
            await RefreshTokens(context, refreshToken);
            await _next(context);
            return;
        }

// Validate the access token and check if it needs refreshing
        var (isValid, expiryTime) = ValidateAccessToken(accessToken);

if (!isValid)
        {
            // Token is invalid, try to refresh
            await RefreshTokens(context, refreshToken);
        }
        else if (expiryTime <= DateTime.UtcNow.AddMinutes(20))
        {
            // Token is valid but expiring soon, refresh it
            await RefreshTokens(context, refreshToken);
        }

// If token is valid and not expiring soon, continue
        await _next(context);
    }

private (bool isValid, DateTime expiryTime) ValidateAccessToken(string accessToken)
    {
        try
        {
            var tokenHandler = new JwtSecurityTokenHandler();

var principal = tokenHandler.ValidateToken(accessToken, _tokenValidationParameters, out var validatedToken);

return (true, validatedToken.ValidTo);
        }
        catch (SecurityTokenValidationException)
        {
            return (false, DateTime.MinValue);
        }
        catch (Exception)
        {
            return (false, DateTime.MinValue);
        }
    }

private async Task RefreshTokens(HttpContext context, string refreshToken)
    {
        try
        {
            // Move these to configuration
            var authUrl = "{YOUR_AUTH_URL}";
            var clientId = "{YOUR_CLIENT_ID}";
            var clientSecret = "{YOUR_CLIENT_SECRET}";

var client = _httpClientFactory.CreateClient();
            var tokenEndpoint = $"{authUrl}/propelauth/oauth/token";

var requestBody = new Dictionary<string, string>
            {
                { "grant_type", "refresh_token" },
                { "refresh_token", refreshToken },
                { "client_id", clientId },
                { "client_secret", clientSecret }
            };

var request = new HttpRequestMessage(HttpMethod.Post, tokenEndpoint)
            {
                Content = new FormUrlEncodedContent(requestBody)
            };

var response = await client.SendAsync(request);

if (response.IsSuccessStatusCode)
            {
                var responseContent = await response.Content.ReadAsStringAsync();
                var tokenData = JsonDocument.Parse(responseContent).RootElement;

var newAccessToken = tokenData.GetProperty("access_token").GetString();
                var newRefreshToken = tokenData.GetProperty("refresh_token").GetString();

// Overwrite the old cookies with the new token data
                context.Response.Cookies.Append("__pa_at", newAccessToken, new CookieOptions()
                {
                    HttpOnly = true,
                    SameSite = SameSiteMode.Lax,
                    IsEssential = true,
                    Secure = true,
                    MaxAge = TimeSpan.FromMinutes(25),
                });

context.Response.Cookies.Append("__pa_rt", newRefreshToken, new CookieOptions()
                {
                    HttpOnly = true,
                    SameSite = SameSiteMode.Lax,
                    IsEssential = true,
                    Secure = true,
                    MaxAge = TimeSpan.FromDays(14),
                });
            }
            else
            {
                // If the refresh token is also invalid, clear cookies
                context.Response.Cookies.Delete("__pa_at");
                context.Response.Cookies.Delete("__pa_rt");
            }
        }
        catch (Exception ex)
        {
            // Log the exception
            Console.WriteLine($"There was an error in refreshing the access token: {ex}");
        }
    }
}
```

Make sure to replace `{YOUR_AUTH_URL}`, `{YOUR_CLIENT_ID}`, and `{YOUR_CLIENT_SECRET}` with the values we retrieved earlier in this guide.

We then need to register the middleware in our `Program.cs` file as well as an httpClient factory to make HTTP requests to the PropelAuth API:

### Program.cs

```csharp
builder.Services.AddHttpClient();

//
//

app.UseMiddleware<TokenRefreshMiddleware>();
```

## [Protecting Pages](https://docs.propelauth.com/getting-started/additional-framework-guides/dot-net-oauth-guide#protecting-pages)

To protect your pages in a Blazor app, you can use the `[Authorize]` attribute to require users to be logged in. For example, you can add `@attribute [Authorize]` to the top of a page.

```razor
@page "/"
@attribute [Authorize]
```

You will also need to add the following imports to your `_Imports.razor` file:

```razor
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
```

When a user navigates to that page they'll automatically be redirected to your login page!

## [Displaying User Information](https://docs.propelauth.com/getting-started/additional-framework-guides/dot-net-oauth-guide#displaying-user-information)

To display user information, you can inject the `AuthenticationStateProvider` into your component and retrieve the user's claims. The `ClaimsPrincipal` class contains the user's claims, which you can then convert to a [User Class](https://docs.propelauth.com/reference/backend-apis/dot-net#user-class) provided by PropelAuth.

The [User Class](https://docs.propelauth.com/reference/backend-apis/dot-net#user-class) contains information about the user that is logged in. It also contains additional methods such as `GetOrgs()` and `HasPermission()`. You must first retrieve the ClaimsPrincipal Class and then convert it to a User Class.

```razor
@page "/"
@attribute [Authorize]
@inject AuthenticationStateProvider AuthenticationStateProvider
@using PropelAuth.Models

<PageTitle>Home</PageTitle>

<h2>User Profile</h2>

<div>
    <p><strong>Email:</strong> @User.Email</p>
    <p><strong>User ID:</strong> @User.UserId</p>
    <p><strong>Org:</strong> @(User.GetOrgs().Any() ? User.GetOrgs().First().OrgName : "No organization")</p>
</div>

@code {
    private PropelAuth.Models.User? User { get; set; }

protected override async Task OnInitializedAsync()
    {
        var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
        var claimsPrincipal = authState.User;
        User = claimsPrincipal.GetUser();
    }
}
```

## [Logging Out](https://docs.propelauth.com/getting-started/additional-framework-guides/dot-net-oauth-guide#logging-out)

Logging a user out of your application requires two parts - logging out of PropelAuth and then deleting the `__pa_at` and `__pa_rt` cookies. To do so, start by creating an endpoint in your backend that will make a request to the PropelAuth [logout API](https://docs.propelauth.com/reference/api/oauth2#logout-endpoint). If successful it will delete the two cookies and redirect the user to your homepage.

### Program.cs

```csharp
app.MapPost("/auth/logout", async (
    HttpContext context,
    IHttpClientFactory httpClientFactory) =>
{
    var refreshToken = context.Request.Cookies["__pa_rt"];

var httpClient = httpClientFactory.CreateClient();
    var requestBody = new { refresh_token = refreshToken };

try
    {
        var response = await httpClient.PostAsJsonAsync($"{AUTH_URL}/api/backend/v1/logout", requestBody);

if (!response.IsSuccessStatusCode)
        {
            Console.WriteLine($"Failed to revoke refresh token. Status: {response.StatusCode}");
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Exception while calling external logout API: {ex.Message}");
    }

context.Response.Cookies.Delete("__pa_rt");
    context.Response.Cookies.Delete("__pa_at");

return Results.Redirect("/");
});
```

In your .razor file, add a button that will send a POST request to the endpoint we just created.

```razor
<form action="/auth/logout" method="post">
    <button type="submit">Logout</button>
</form>
```

And that's it! You have now set up your .NET application to use PropelAuth for authentication.

## [Upgrading to PropelAuth .NET v0.2.0](https://docs.propelauth.com/getting-started/additional-framework-guides/dot-net-oauth-guide#upgrading-to-propel-auth-net-v0-2-0)

Authentication should be as easy as possible while also getting out of your way. To give you more control over your authentication experience, version 0.2.0 introduces a breaking change to how you initialize the PropelAuth .NET package. Previously, initialization looked like this:

```csharp
using System.Security.Claims;
using PropelAuth;
using PropelAuth.Models;

var builder = WebApplication.CreateBuilder(args);

await builder.Services.AddPropelAuthAsync(new PropelAuthOptions(
    apiKey: "YOUR_API_KEY",
    authUrl: "YOUR_AUTH_URL",
    // if using OAuth
    oAuthOptions: new PropelAuth.Models.OAuthOptions(
        clientId: "YOUR_CLIENT_ID",
        clientSecret: "YOUR_CLIENT_SECRET",
        callbackPath: "/auth/callback",
        allowBearerTokenAuth: true
    )
));
```

With this update, we're giving you the flexibility to define how your application handles access token validation and integrates with OAuth, directly within your configuration. If you're using PropelAuth's .NET library for backend access token validation, you can now streamline your setup. Simply replace your previous initialization with the following:

```csharp
using System.Security.Cryptography;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

var AUTH_URL = "{YOUR_AUTH_URL}";
var PUBLIC_KEY = "{YOUR_PUBLIC_VERIFIER_KEY}";

var rsa = RSA.Create();
rsa.ImportFromPem(PUBLIC_KEY);

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateAudience = false,
        ValidAlgorithms = new List<string>() {"RS256"},
        ValidIssuer = AUTH_URL,
        IssuerSigningKey = new RsaSecurityKey(rsa),
        ValidateLifetime = true,
        ClockSkew = TimeSpan.Zero
    };
});
```

If you have any questions, please do not hesitate to reach out to **[support@propelauth.com](mailto:support@propelauth.com)**!
