Giter VIP home page Giter VIP logo

aspnetcore-keycloak's Introduction

ASP.NET Core - Keycloak authorization guide

Introduction

This repo is for anyone interrested in adding Keycloak authorization to an ASP.NET Core app with OIDC.
I had troubles finding resources about the subject for ASP.NET Core and got quite a lot of grey hair in the process.
I ended up cracking the issue and created this solution and guide to help anyone having the same problem.
Feel free to edit the code, by submitting a PR.
May the code be with you ☺️

Start

Clone the repo and choose if you want the dotnet 3 or dotnet 6 project, then open KeycloakAuth.sln in Visual Studio or VS Code.

Appsettings.json

In order to get the application to work, you need to configure the application.json with the appropriate Keycloak metadata from your environment.
Login to your Keycloak admin page, to get the the needed varibles.

Name Example Value Docker env name
ServerRealm https://keycloak.example.com/auth/realms/keycloak-realm Keycloak__ServerRealm
Metadata https://keycloak.example.com/auth/realms/keycloak-realm/.well-known/openid-configuration Keycloak__Metadata
ClientId Keycloak ClientId Keycloak__ClientId
ClientSecret Keycloak ClientSecret Keycloak__ClientSecret

Policy vs. Roles

The code uses a policy example, but also comments on how to use roles within the code.
The major difference is that based on claims you can create certain policies, that only users with claim x, y and z can access.
If you have 15 different roles in your app, the authorize attribute can be quite confusing to read for each controller action.
Use whatever suit your needs, the examples should be there.

Keycloak Configuration

In order to get authorization to work with Keycloak, you will need to add a new role to Client Scopes.

  1. Login to Keycloak Admin page
  2. Goto Client Scopes
  3. Goto 'roles' entry
  4. Goto Mappers
  5. Click Add mapper -> By configuration -> User Client Role
  6. Give the new role a name
  7. Multivated must be on
  8. Token claim name must be 'role'
  9. Add to access token must be on

User roles : To test the application Authentication policy options, at least one user will need to have the role 'admin'.

Keycloak Client Scope

Login/Logout

I needed a silent authorization, there is no login or logout function built in.
You can probably find other examples on github, where they do this.

Token exchange

There is a token exchange example in KeycloakAuthDotNet6 that needs to be uncommented in the HomeController, it implements middleware handling the exchange token and refresh token creation.
Official documentation on token exchange: https://www.keycloak.org/docs/latest/securing_apps/#_token-exchange
The appsettings needs to be updated with the Audience and token exchange point.

Name Example Value Docker env name
TokenExchange https://keycloak.example.com/auth/realms/keycloak-realm/protocol/openid-connect/token Keycloak__TokenExchange
Audience Client role connected to the service that needs to be token exchanged fx. "example-service", see https://www.keycloak.org/docs/latest/securing_apps/#internal-token-to-internal-token-exchange for more info Keycloak__Audience

Depending on your token lifetime settings, it might be a good idea to do a refresh token before calling the token-exchange endpoint.

  1. Obtain an access token from the client
  2. Use the refresh token, to get a new access token
  3. Use the new access token, to do a token exchange on the audience connected to the other keycloak client Remember to grant the nessesary roles and permissions on both clients.

Dotnet 6

In order to see tokens/claims in dotnet 6, you will have to install the package System.IdentityModel.Tokens.Jwt.
For some reason, it's not updated in the authentication dependency.

Docker

In the repo, there is a dockerfile for the dotnet6 version, that can be used to build an image.
If you are using another version, just find the correct image tag in docker hub.
If you are using http and not https, you will need to change the aspnetcore ports accordingly in the file.
Override environment variables with -e varName="someVar", see appsettings.json for the names to override.

Docker build . -t keycloakauth
Docker run -it --rm -p 5001:5001 keycloakauth

Troubleshooting

Invalid redirect URI

Keycloak tells you "invalid redirect uri" - you need to add your apps uri ex: https://localhost:44556 to the valid redirect URIs and web origins.

Keycloak URI

Access denied

You are presented with the access denied page.
Copy your access token from the HomeController to jwt.io and look for what claims you have.
They need to match the role names configured in Keycloak and in the policy.
If you use Active Directory, sometimes the sync is very slow, renew your kerberos token and restart Keycloak or force a sync.

Keycloak Roles

Token exchange failing

You are not granted an exchange token for your service.
Insert a breakpoint in the TokenExchange.cs file, where the access token is returned and verify the claims are correct by validating the token in jwt.io.
You should see, that the claims and settings belong to the exchanged client.
If this is correct, validate the settings in Keycloak are correct, and that the service has permissions to exchange a token on service X.

aspnetcore-keycloak's People

Contributors

gldraphael avatar pgrondin27 avatar tuxiem avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

aspnetcore-keycloak's Issues

The SSL connection could not be established, see inner exception

Can some one help me with this one? (.net 6 - Ubuntu)

IOException: Cannot determine the frame size or a corrupted frame was received.
System.Net.Security.SslStream.GetFrameSize(ReadOnlySpan<byte> buffer)

HttpRequestException: The SSL connection could not be established, see inner exception.
System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, bool async, Stream stream, CancellationToken cancellationToken)

IOException: IDX20804: Unable to retrieve document from: '[PII of type 'System.String' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
Microsoft.IdentityModel.Protocols.HttpDocumentRetriever.GetDocumentAsync(string address, CancellationToken cancel)

InvalidOperationException: IDX20803: Unable to obtain configuration from: '[PII of type 'System.String' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]'.
Microsoft.IdentityModel.Protocols.ConfigurationManager<T>.GetConfigurationAsync(CancellationToken cancel)

IOException: Cannot determine the frame size or a corrupted frame was received.

System.Net.Security.SslStream.GetFrameSize(ReadOnlySpan<byte> buffer)
System.Net.Security.SslStream.ReceiveBlobAsync<TIOAdapter>(TIOAdapter adapter)
System.Threading.Tasks.ValueTask<TResult>.get_Result()
System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable<TResult>+ConfiguredValueTaskAwaiter.GetResult()
System.Net.Security.SslStream.ForceAuthenticationAsync<TIOAdapter>(TIOAdapter adapter, bool receiveFirst, byte[] reAuthenticationData, bool isApm)
System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, bool async, Stream stream, CancellationToken cancellationToken)

HttpRequestException: The SSL connection could not be established, see inner exception.

System.Net.Http.ConnectHelper.EstablishSslConnectionAsync(SslClientAuthenticationOptions sslOptions, HttpRequestMessage request, bool async, Stream stream, CancellationToken cancellationToken)
System.Threading.Tasks.ValueTask<TResult>.get_Result()
System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable<TResult>+ConfiguredValueTaskAwaiter.GetResult()
System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
System.Threading.Tasks.ValueTask<TResult>.get_Result()
System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable<TResult>+ConfiguredValueTaskAwaiter.GetResult()
System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
System.Threading.Tasks.ValueTask<TResult>.get_Result()
System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable<TResult>+ConfiguredValueTaskAwaiter.GetResult()
System.Net.Http.HttpConnectionPool.AddHttp11ConnectionAsync(HttpRequestMessage request)
System.Threading.Tasks.TaskCompletionSourceWithCancellation<T>.WaitWithCancellationAsync(CancellationToken cancellationToken)
System.Threading.Tasks.ValueTask<TResult>.get_Result()
System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable<TResult>+ConfiguredValueTaskAwaiter.GetResult()
System.Net.Http.HttpConnectionPool.GetHttp11ConnectionAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
System.Threading.Tasks.ValueTask<TResult>.get_Result()
System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable<TResult>+ConfiguredValueTaskAwaiter.GetResult()
System.Net.Http.HttpConnectionPool.SendWithVersionDetectionAndRetryAsync(HttpRequestMessage request, bool async, bool doRequestAuth, CancellationToken cancellationToken)
System.Net.Http.DiagnosticsHandler.SendAsyncCore(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
System.Threading.Tasks.ValueTask<TResult>.get_Result()
System.Runtime.CompilerServices.ConfiguredValueTaskAwaitable<TResult>+ConfiguredValueTaskAwaiter.GetResult()
System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken)
System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, bool disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)
Microsoft.IdentityModel.Protocols.HttpDocumentRetriever.GetDocumentAsync(string address, CancellationToken cancel)

IOException: IDX20804: Unable to retrieve document from: '[PII of type 'System.String' is hidden. For more details, see

https://aka.ms/IdentityModel/PII
Microsoft.IdentityModel.Protocols.HttpDocumentRetriever.GetDocumentAsync(string address, CancellationToken cancel)
Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectConfigurationRetriever.GetAsync(string address, IDocumentRetriever retriever, CancellationToken cancel)
Microsoft.IdentityModel.Protocols.ConfigurationManager<T>.GetConfigurationAsync(CancellationToken cancel)

InvalidOperationException: IDX20803: Unable to obtain configuration from: '[PII of type 'System.String' is hidden. For more details

see https://aka.ms/IdentityModel/PII.
Microsoft.IdentityModel.Protocols.ConfigurationManager<T>.GetConfigurationAsync(CancellationToken cancel)
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleChallengeAsyncInternal(AuthenticationProperties properties)
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleChallengeAsync(AuthenticationProperties properties)
Microsoft.AspNetCore.Authentication.AuthenticationHandler<TOptions>.ChallengeAsync(AuthenticationProperties properties)
Microsoft.AspNetCore.Authentication.AuthenticationService.ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties)
Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

KeyCloak configuration

Whilst I get the project to compile, I consistently get an access denied error. This leads me to believe that something is not configured correctly on the keycloak console side. The readme doesnt contain what to actually configure with regards to roles, policies, claims, etc. The only mention of this is the Role Mapper but nothing else. Can you please provide how to configure a user in its entirety over and above the role mapper?

Request for advice on how to update context Tokens after refresh routine

Hi! Awesome work on this guide. I'm looking at ASP Net Core 7 version of the project which you added recently.

How to update the outcome of await context.GetTokenAsync("access_token")?

I have sort of middleware similar to below which takes care of renewing tokens if necessary:

    public async Task InvokeAsync(HttpContext context)
    {
        // if not authenticated then just execute to next middleware and return
        if (context.User.Identity?.IsAuthenticated != true)
        {
            await _next(context);
            return;
        }
        
        // get username, for keycloak you need to regex this to get the clean username
        var currentUserName = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        
        // logs an error so it's easier to find - thanks debug.
        if (currentUserName != null) _logger.LogInformation($"Current user: {currentUserName}");

        //Debug this line of code if you want to validate the content jwt.io
        var accessToken = await context.GetTokenAsync("access_token");
        var idToken = await context.GetTokenAsync("id_token");
        var refreshToken = await context.GetTokenAsync("refresh_token");
        
        // check if accessToken is expired
        if (IsTokenExpired(accessToken))
        {
            var newAccessToken = await GetRefreshTokenAsync(refreshToken);
            var serviceAccessToken = await GetTokenExchangeAsync(newAccessToken);
            _logger.LogInformation("Access token is expired, refreshing token");

            // how to update the outcome of `await context.GetTokenAsync("access_token")`?
        }
        
        // Call the next delegate/middleware in the pipeline.
        await _next(context);
    }

Full code for middleware: https://gist.github.com/bangonkali/f3bf3641653bc41e24dd7560bd2e6c74

You can then apply this on your app using the following:

app.UseTokenExchange();

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.