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

Feature - SSO keycloak #5691

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions argilla-frontend/components/features/login/OAuthLogin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
v-if="provider.isHuggingFace"
@click="authorize(provider.name)"
/>
<KeycloakButton
v-if="provider.isKeycloak"
@click="authorize(provider.name)"
/>
</div>
</div>
</template>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<template>
<BaseButton class="button-kc" @click="$emit('click')">
<KeycloakLogo />
{{ $t("button.kc-login") }}
</BaseButton>
</template>
<script>
export default {
name: "KeycloakButton",
};
</script>

<style lang="scss" scoped>
.button-kc {
@extend %button !optional;
background: var(--color-black);
color: var(--color-white);
width: 100%;
padding: calc($base-space / 2) $base-space * 4;
justify-content: center;
&:hover {
background: hsl(from var(--color-black) h s l / 80%);
}
svg {
width: 30px;
height: auto;
}
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<template>
<!--https://github.com/keycloak/keycloak-misc/blob/main/logo/icon.svg-->
<svg width="256" height="256" viewBox="0 0 44.216 39.861" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m88.61 138.456 5.716-9.865 23.018-.004 5.686 9.965.007 19.932-5.691 9.957-23.012.008-5.782-9.965z"
style="display:inline;fill:#4d4d4d;fill-opacity:1;stroke-width:.264583"
transform="translate(-82.815 -128.588)" />
<path d="M88.552 158.481h10.375l-5.699-10.041 4.634-9.982-9.252-.002-5.795 10.065"
style="fill:#ededed;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729"
transform="translate(-82.815 -128.588)" />
<path d="M102.073 158.481h7.582l6.706-9.773-6.589-10.156h-8.921l-5.373 9.814z"
style="fill:#e0e0e0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729"
transform="translate(-82.815 -128.588)" />
<path d="m82.815 148.52 5.738 9.964h10.374l-5.636-9.93z"
style="fill:#acacac;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729"
transform="translate(-82.815 -128.588)" />
<path d="m95.589 148.522 6.484 9.963h7.582l6.601-9.959z"
style="fill:#9e9e9e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729"
transform="translate(-82.815 -128.588)" />
<path d="m98.157 148.529-1.958.569-1.877-.572 7.667-13.288 1.918 3.316"
style="fill:#00b8e3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729"
transform="translate(-82.815 -128.588)" />
<path d="m103.9 158.482-1.909 3.332-5.093-5.487-2.58-7.797v-.004h3.838"
style="fill:#33c6e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729"
transform="translate(-82.815 -128.588)" />
<path d="M94.322 148.526h-.003v.003l-1.918 3.322-1.925-3.307 1.952-3.386 5.728-9.92h3.834"
style="fill:#008aaa;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729"
transform="translate(-82.815 -128.588)" />
<path d="M115.42 158.481h11.611l-.007-19.93h-11.605z"
style="fill:#d4d4d4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729"
transform="translate(-82.815 -128.588)" />
<path d="M115.42 148.554v9.93h11.59v-9.93z"
style="fill:#919191;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729"
transform="translate(-82.815 -128.588)" />
<path d="M101.992 161.817h-3.836l-5.755-9.966 1.918-3.321z"
style="fill:#00b8e3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729"
transform="translate(-82.815 -128.588)" />
<path d="m117.333 148.526-7.669 13.289c-.705-1.036-1.913-3.331-1.913-3.331l5.753-9.959z"
style="fill:#008aaa;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729"
transform="translate(-82.815 -128.588)" />
<path
d="m113.495 161.815-3.831-.001 7.67-13.288 1.917-3.317 1.921 3.34m-3.839-.023h-3.828l-5.755-9.973 1.905-3.314 4.658 5.922z"
style="fill:#00b8e3;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729"
transform="translate(-82.815 -128.588)" />
<path d="M119.25 145.205v.003l-1.917 3.318-7.677-13.286 3.841.002z"
style="fill:#33c6e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:.330729"
transform="translate(-82.815 -128.588)" />
</svg>
</template>
1 change: 1 addition & 0 deletions argilla-frontend/translation/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export default {
ignore_and_continue: "Ignorieren und fortfahren",
login: "Anmelden",
"hf-login": "Mit Hugging Face anmelden",
"kc-login": "Mit Keycloak anmelden",
sign_in_with_username: "Mit Benutzername anmelden",
cancel: "Abbrechen",
continue: "Fortfahren",
Expand Down
1 change: 1 addition & 0 deletions argilla-frontend/translation/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export default {
ignore_and_continue: "Ignore and continue",
login: "Sign in",
"hf-login": "Sign in with Hugging Face",
"kc-login": "Sign in with Keycloak",
sign_in_with_username: "Sign in with username",
cancel: "Cancel",
continue: "Continue",
Expand Down
1 change: 1 addition & 0 deletions argilla-frontend/translation/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export default {
ignore_and_continue: "Ignorar y continuar",
login: "Iniciar sesión",
"hf-login": "Iniciar sesión con Hugging Face",
"hf-login": "Iniciar sesión con Keycloak",
sign_in_with_username: "Iniciar sesión con usuario",
cancel: "Cancelar",
continue: "Continuar",
Expand Down
5 changes: 4 additions & 1 deletion argilla-frontend/v1/domain/entities/oauth/OAuthProvider.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Dictionary } from "../common/Params";

export type ProviderType = "huggingface";
export type ProviderType = "huggingface" | "keycloak" ;
export type OAuthParams = Dictionary<string | (string | null)[]>;
export class OAuthProvider {
constructor(public readonly name: ProviderType) {}

get isHuggingFace() {
return this.name === "huggingface";
}
get isKeycloak() {
return this.name === "keycloak";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,20 @@
from argilla_server.security.authentication.oauth2.providers._base import OAuth2ClientProvider
from argilla_server.security.authentication.oauth2.providers._github import GitHubClientProvider
from argilla_server.security.authentication.oauth2.providers._huggingface import HuggingfaceClientProvider
from argilla_server.security.authentication.oauth2.providers._keycloak import KeycloakClientProvider

__all__ = [
"OAuth2ClientProvider",
"GitHubClientProvider",
"HuggingfaceClientProvider",
"KeycloakClientProvider",
"get_provider_by_name",
]

_ALL_SUPPORTED_OAUTH2_PROVIDERS = {
GitHubClientProvider.name: GitHubClientProvider,
HuggingfaceClientProvider.name: HuggingfaceClientProvider,
KeycloakClientProvider.name: KeycloakClientProvider,
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Copyright 2021-present, the Recognai S.L. team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
from typing import Optional, List

from social_core.backends.keycloak import KeycloakOAuth2
from social_core.backends.open_id_connect import OpenIdConnectAuth

from argilla_server.logging import LoggingMixin
from argilla_server.security.authentication.claims import Claims
from argilla_server.security.authentication.oauth2.providers._base import OAuth2ClientProvider

_LOGGER = logging.getLogger("argilla.security.oauth2.providers.keycloak")


class KeycloakOpenId(OpenIdConnectAuth):
"""Huggingface OpenID Connect authentication backend."""

name = "keycloak"

@staticmethod
def from_oidc_endpoint(oidc_endpoint: str):
KeycloakOpenId.OIDC_ENDPOINT = oidc_endpoint.rstrip("/")
KeycloakOpenId.AUTHORIZATION_URL = f"{oidc_endpoint}/protocol/openid-connect/auth"
KeycloakOpenId.ACCESS_TOKEN_URL = f"{oidc_endpoint}/protocol/openid-connect/token"

return KeycloakOpenId

def oidc_endpoint(self) -> str:
return self.OIDC_ENDPOINT


class KeycloakClientProvider(OAuth2ClientProvider, LoggingMixin):
def __init__(
self,
client_id: str = None,
client_secret: str = None,
scope: Optional[List[str]] = None,
redirect_uri: str = None,
oidc_endpoint: str = None,
):
if oidc_endpoint is None:
raise ValueError("oidc_endpoint needs to be set in the Keycloak configuration")

self.oidc_endpoint = oidc_endpoint
self.backend_class = KeycloakOpenId.from_oidc_endpoint(self.oidc_endpoint)
print(self.backend_class.__dict__)
super().__init__(
client_id=client_id,
client_secret=client_secret,
scope=scope,
redirect_uri=redirect_uri,
)

claims = Claims(
identity=lambda user: f"{user.provider}:{user.id}",
username="preferred_username",
# TODO . 'given_name': 'xy', 'family_name': 'xy'
)
name = "keycloak"
131 changes: 131 additions & 0 deletions docs/_source/tutorials_and_integrations/integrations/sso_keycloak.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This docs folder is outdated. For 2.x docs you should use the argilla/docs folder.

Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# SSO Integration Keycloak

To test this run a test version of Keycloak in Docker:

```bash
docker run -p 8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:26.0.5 start-dev
```

After that you might want to create a new realm and a client for Argilla to use. The client should expose the client audience via userinfo. And then add a user.
The script below should do all of that for you to test.

```python
from keycloak import KeycloakAdmin
from keycloak import KeycloakOpenIDConnection
from keycloak import KeycloakOpenID

keycloak_connection = KeycloakOpenIDConnection(
server_url="http://localhost:8080/",
username="admin",
password="admin",
realm_name="master",
client_id="admin-cli",
)

keycloak_admin = KeycloakAdmin(connection=keycloak_connection)

keycloak_admin.create_realm(
{
"realm": "argilla",
"enabled": True,
"displayName": "Argilla",
"userManagedAccessAllowed": True,
}
)
keycloak_connection = KeycloakOpenIDConnection(
server_url="http://localhost:8080/",
username="admin",
password="admin",
user_realm_name="master",
realm_name="argilla",
)

keycloak_admin = KeycloakAdmin(connection=keycloak_connection)

new_user = keycloak_admin.create_user(
{
"email": "[email protected]",
"username": "example",
"enabled": True,
"firstName": "Example",
"lastName": "User",
"credentials": [
{
"value": "secret",
"type": "password",
}
],
}
)

client = keycloak_admin.create_client(
{
"clientId": "example-client", # The client ID (you can choose a name)
"enabled": True,
"protocol": "openid-connect", # Protocol (you can use other protocols like 'saml' if needed)
"publicClient": False, # Set to False if the client will use client secrets
"directAccessGrantsEnabled": True,
"standardFlowEnabled": True,
"frontchannelLogout": True,
"secret": "client-secret", # Set a secret if it's not a public client
"redirectUris": [
"http://localhost:3000/*",
"http://localhost:6900/*",
], # Redirect URIs after authentication
}
)

keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/",
client_id="example-client",
realm_name="argilla")

public_key = keycloak_openid.public_key()

client_scope = keycloak_admin.create_client_scope({
"name": "example-client-scope_3",
"protocol": "openid-connect"
})

# Create Audience Mapper
mapper = keycloak_admin.add_mapper_to_client_scope(
client_scope_id=client_scope,
payload={
"name": "Client Audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": False,
"config": {
"included.client.audience": "example-client",
"id.token.claim": "false",
"access.token.claim": "true"
}
})

keycloak_admin.add_default_default_client_scope(client_scope)
```

After that you need to configure you endpoints in the `.oauth.yaml` same as this is done for the HuggingFace Oauth:

```yaml
# Change to `false` to disable HF oauth integration
#enabled: false

allow_http_redirect: true

providers:
- name: keycloak
client_id: <name of your client e.g. example-client>
client_secret: <value of your specified secret e.g. client-secret>
redirect_uri: http://0.0.0.0:3000/oauth/keycloak/callback
oidc_endpoint: http://localhost:8080/realms/<name of your realm e.g. argilla>
- name: huggingface
client_id: <create a new https://huggingface.co/settings/connected-applications>
client_secret: <create a new https://huggingface.co/settings/connected-applications>
redirect_uri: http://0.0.0.0:3000/oauth/huggingface/callback # if you test locally

# Allowed workspaces must exists
allowed_workspaces:
- name: default
```

And you are good to go