Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Keycloak to providers #1261

Closed
manuschillerdev opened this issue Feb 5, 2021 · 12 comments
Closed

Adding Keycloak to providers #1261

manuschillerdev opened this issue Feb 5, 2021 · 12 comments
Labels
enhancement New feature or request stale Did not receive any activity for 60 days

Comments

@manuschillerdev
Copy link

manuschillerdev commented Feb 5, 2021

Summary of proposed feature
Keycloak is a great, lightweight open source auth provider that can easily be self hosted. This feature adds a default Provider for using keycloak with next-auth and OpenID2. Feature request originated from #1195

Purpose of proposed feature
A clear and concise description description of why this feature is necessary and what problems it solves.
The feature solves the problem of unknown configs in regard of using keycloak with next-auth.

Detail about proposed feature
Keycloak basically has two dynamic parts in its oauth config urls. The domain and the realm name:
e.g. https://${options.domain}/auth/realms/${realm}/protocol/openid-connect/token

I would propose that it can be configured similarly to the IdentityServer4Provider and add the options.realm attribute to it.
In most of the cases simple Keycloak setups only use one realm (master) so it can be discussed, if options.realm should set to master by default.

// src/providers/keycloak.js
export default ({ realm = "master", ...options }) => {
  return {
    id: "keycloak",
    name: "Keycloak",
    type: "oauth",
    version: "2.0",
    params: { grant_type: "authorization_code" },
    scope: "openid profile email",
    accessTokenUrl: `https://${options.domain}/auth/realms/${realm}/protocol/openid-connect/token`,
    requestTokenUrl: `https://${options.domain}/auth/realms/${realm}/protocol/openid-connect/auth`,
    authorizationUrl: `https://${options.domain}/auth/realms/${realm}/protocol/openid-connect/auth?response_type=code`,
    profileUrl: `https://${options.domain}/auth/realms/${realm}/protocol/openid-connect/userinfo`,
    profile: (profile) => ({ ...profile, id: profile.sub }),
    ...options,
  };
};

demo of currently working version:

Screen.Recording.2021-02-05.at.11.05.27.mov

Potential problems
In local environments you often use Keycloak via docker-compose and serve the application via http instead of https.
For these cases you would need to set all URLs separately again only for the DEV-Environment, because the default urls in the provider do not allow to change the protocol. Please compare the IdentityServer4Provider implementation, which currently has the same problem locally.

    // pages/api/auth/[...nextauth].js
    Providers.Keycloak({
      domain: process.env.KEYCLOAK_DOMAIN,
      clientId: "nextjs_local",
      clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
      // following lines are only needed in dev environments, 
      // because the protocol can not be changed from http to https via options
      accessTokenUrl: `http://${options.domain}/auth/realms/${realm}/protocol/openid-connect/token`,
      requestTokenUrl: `http://${options.domain}/auth/realms/${realm}/protocol/openid-connect/auth`,
      authorizationUrl: `http://${options.domain}/auth/realms/${realm}/protocol/openid-connect/auth?response_type=code`,
      profileUrl: `http://${options.domain}/auth/realms/${realm}/protocol/openid-connect/userinfo`,
    }),

Describe any alternatives you've considered
We could offer to make the protocol dynamic, but set its default to https:

export default ({ realm = "master", protocol = "https", ...options }) => {
    accessTokenUrl: `${protocol}://${options.domain}/auth/realms/${realm}/protocol/openid-connect/token`,

This would deviate from the other provider APIs, so it should be discussed how the API should look in general (also regarding the IdentityServer4 provider).

Additional context

Local keycloak setup via docker-compose (localhost:8080) for testing (credentials: `admin/admin)`:
version: '3'

volumes:
  postgres_data:
    driver: local
services:
  postgres:
    image: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: keycloak
      POSTGRES_USER: keycloak
      POSTGRES_PASSWORD: password
  keycloak:
    image: quay.io/keycloak/keycloak:latest
    environment:
      DB_VENDOR: POSTGRES
      DB_ADDR: postgres
      DB_DATABASE: keycloak
      DB_USER: keycloak
      DB_SCHEMA: public
      DB_PASSWORD: password
      KEYCLOAK_USER: admin
      KEYCLOAK_PASSWORD: admin
    ports:
      - 8080:8080
    depends_on:
      - postgres
nextjs_local client app config - can be imported via admin UI
{
    "clientId": "nextjs_local",
    "name": "nextjs_local",
    "rootUrl": "http://localhost:3000",
    "adminUrl": "http://localhost:3000",
    "baseUrl": "http://localhost:3000/api/auth/callback/keycloak",
    "surrogateAuthRequired": false,
    "enabled": true,
    "alwaysDisplayInConsole": false,
    "clientAuthenticatorType": "client-secret",
    "redirectUris": [
        "http://localhost:3000/*"
    ],
    "webOrigins": [
        "http://localhost:3000"
    ],
    "notBefore": 0,
    "bearerOnly": false,
    "consentRequired": false,
    "standardFlowEnabled": true,
    "implicitFlowEnabled": false,
    "directAccessGrantsEnabled": true,
    "serviceAccountsEnabled": false,
    "publicClient": false,
    "frontchannelLogout": false,
    "protocol": "openid-connect",
    "attributes": {
        "saml.assertion.signature": "false",
        "access.token.lifespan": "600",
        "saml.force.post.binding": "false",
        "saml.multivalued.roles": "false",
        "saml.encrypt": "false",
        "saml.server.signature": "false",
        "saml.server.signature.keyinfo.ext": "false",
        "exclude.session.state.from.auth.response": "false",
        "saml_force_name_id_format": "false",
        "saml.client.signature": "false",
        "tls.client.certificate.bound.access.tokens": "false",
        "saml.authnstatement": "false",
        "display.on.consent.screen": "false",
        "saml.onetimeuse.condition": "false"
    },
    "authenticationFlowBindingOverrides": {
        "direct_grant": "ffba6037-ec82-4aab-8e02-5adfbdac7715",
        "browser": "7a40d4df-442f-49f3-af30-6d603a470a2d"
    },
    "fullScopeAllowed": false,
    "nodeReRegistrationTimeout": -1,
    "protocolMappers": [
        {
            "name": "client_role",
            "protocol": "openid-connect",
            "protocolMapper": "oidc-usermodel-client-role-mapper",
            "consentRequired": false,
            "config": {
                "multivalued": "true",
                "userinfo.token.claim": "true",
                "id.token.claim": "true",
                "access.token.claim": "true",
                "claim.name": "role",
                "jsonType.label": "String",
                "usermodel.clientRoleMapping.clientId": "nextjs_local"
            }
        }
    ],
    "defaultClientScopes": [
        "web-origins",
        "role_list",
        "roles",
        "profile",
        "email"
    ],
    "optionalClientScopes": [
        "address",
        "phone",
        "offline_access",
        "microprofile-jwt"
    ],
    "access": {
        "view": true,
        "configure": true,
        "manage": true
    }
}

Please indicate if you are willing and able to help implement the proposed feature.
Absolutely. Once the questions are cleared I can happily provide a PR, documentation and a blogpost for it.
I would assume the current information in this PR requires some knowledge about Keycloak on how to set the example up locally for testing. I will try to provide an example repo asap.

@manuschillerdev manuschillerdev added the enhancement New feature or request label Feb 5, 2021
@balazsorban44
Copy link
Member

balazsorban44 commented Feb 5, 2021

So in my opinion, we should not add one-off options that satisfy a single specific provider (see #1022).

If it is possible to add it in a way that is useful for most/many providers, I think it is worth considering. Otherwise, I would like to see if this isn't possible to solve easily in the userland?

// pages/api/auth/[...nextauth].js
...
Providers.Keycloak({
  domain: `${process.env.KEYCLOAK_DOMAIN}/auth/realms/${process.env.KEYCLOAK_REALM}`
  clientId: "nextjs_local",
  clientSecret: process.env.KEYCLOAK_CLIENT_SECRET
}),

And then:

// src/providers/keycloak.js

const protocol = process.env.NODE_ENV !==  "production" ? "http" : "https"
export default (options) => {
  return {
    id: "keycloak",
    name: "Keycloak",
    type: "oauth",
    version: "2.0",
    params: { grant_type: "authorization_code" },
    scope: "openid profile email",
    profile: (profile) => ({ ...profile, id: profile.sub }),
    accessTokenUrl: `${protocol}://${options.domain}/protocol/openid/token`,
    requestTokenUrl: `${protocol}://${options.domain}/protocol/openid/auth`,
    authorizationUrl: `${protocol}://${options.domain}/protocol/openid/auth?response_type=code`,
    profileUrl: `${protocol}://${options.domain}/protocol/openid/userinfo`,
    ...options,
  };
};

This way we don't have to introduce any new options just for the sake of Keycloak.

What do you think?

Actually, I can see that domain is not a documented option https://next-auth.js.org/configuration/providers#oauth-provider-options, so we should probably add it.

@manuschillerdev
Copy link
Author

I agree on keeping the interface for options consistent 👍
But I don't feel to confident in the decision on setting the default protocol based on NODE_ENV.

what's the reason that auth/realms/${process.env.KEYCLOAK_REALM} is part of options.domain, and /protocol/openid/ is part of the actual URLs?

Since keycloak mainly is used as self hosted solution maybe it just doesn't make sense to include some kind of default with dynamic parts, but rather keeping it simple and leave the decision up to the user?

So:

export default (options) => {
  return {
    id: "keycloak",
    name: "Keycloak",
    type: "oauth",
    version: "2.0",
    params: { grant_type: "authorization_code" },
    scope: "openid profile email",
    profile: (profile) => ({ ...profile, id: profile.sub }),
    ...options,
  };
};
// pages/api/auth/[...nextauth].js
...
Providers.Keycloak({
  clientId: "nextjs_local",
  clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
  accessTokenUrl: `${process.env.KEYCLOAK_BASE_URL}/token`,
  requestTokenUrl: `${process.env.KEYCLOAK_BASE_URL}/auth`,
  authorizationUrl: `${process.env.KEYCLOAK_BASE_URL}/auth?response_type=code`,
  profileUrl: `${process.env.KEYCLOAK_BASE_URL}/userinfo`
}),
KEYCLOAK_BASE_URL=http://localhost:8080/auth/realms/master/protocol/openid-connect

it would not use domain, but I think that would be alright.

What do you think?

@balazsorban44
Copy link
Member

Yes, it is certainly the easiest/fastest way! I think we could add it as you suggested, and come back to it later on if we can provide it with fewer options.

@iaincollins
Copy link
Member

Something closer to the design from @balazsorban44 makes sense to me it appreciate I'm not up to speed on typical Keycloak implementations.

Seems like it's worth referencing existing implementations done for v1 (which I worked on but can't remember much about now) and perhaps in Passport.js Strategies and in Keycloak docs to see if they assume sensible defaults so configuration can be as simple as possible in the majority of cases.

This is actually not a challenge unique to Keycloak, all provider properties can be overridden as options but most of the time that's a minority scenario; if there are sensible default parts of paths we should treat those as assumed defaults (and show how to override them in the docs). Some of the existing providers might also provide inspiration.

@manuschillerdev
Copy link
Author

@iaincollins totally agree. it would be awesome to have sensible defaults for the adapter. But i fear I don't know enough yet about either keycloak or next-auth to be a big help.
If you have any idea I will happily include it in the PR :)

@stale
Copy link

stale bot commented Apr 8, 2021

Hi there! It looks like this issue hasn't had any activity for a while. It will be closed if no further activity occurs. If you think your issue is still relevant, feel free to comment on it to keep it open. (Read more at #912) Thanks!

@stale stale bot added the stale Did not receive any activity for 60 days label Apr 8, 2021
@stale
Copy link

stale bot commented Apr 15, 2021

Hi there! It looks like this issue hasn't had any activity for a while. To keep things tidy, I am going to close this issue for now. If you think your issue is still relevant, just leave a comment and I will reopen it. (Read more at #912) Thanks!

@stale stale bot closed this as completed Apr 15, 2021
@OliverDudgeon
Copy link

I'd still like to see Keycloak added to the built-in providers.

@softshipper
Copy link

@manuschillerdev
Could you please provide an example of how to integrate next-auth to Keycloak? Example with the resource.

Thanks

@Senseye
Copy link

Senseye commented May 14, 2021

Also interested in having a Keycloak Provider.
I've got the login working with a custom provider for now, but having issues refreshing the token.

Can anyone share their working KC config?

@manuschillerdev
Copy link
Author

I could try to set up a blog post about it. It will take me a week, though - sorry!

@Senseye
Copy link

Senseye commented May 17, 2021

I could try to set up a blog post about it. It will take me a week, though - sorry!

Cool! I hope it will help some people around here.

I've fixed the token rotation issue by the way, here's the correct request:

const response = await fetch(TOKEN_ENDPOINT, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        client_id: CLIENT_ID,
        client_secret: CLIENT_SECRET,
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
      }),
      method: 'POST',
    })

    const refreshedTokens = await response.json()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request stale Did not receive any activity for 60 days
Projects
None yet
Development

No branches or pull requests

6 participants