Giter Club home page Giter Club logo

Comments (11)

Captain-P-Goldfish avatar Captain-P-Goldfish commented on August 15, 2024

the SCIM endpoint are indeed not respecting the UMA protocol. A failed authentication attempt is simply responded with 401 unauthenticated or if successfully authenticated with a 403 forbidden if the necessary roles for accessing the resources are missing.

What is it you want to achieve by using the UMA protocol on the SCIM endpoints? I have considered the SCIM endpoints to be in an administrational function only to be used by very few authorized parties. What is your use-case?

from scim-for-keycloak.

jacen05 avatar jacen05 commented on August 15, 2024

OK, thanks for your answer.
The need is only to make SCIM to work correctly in my app, replacing an existing Gluu server with Keycloak.
The use-case is that the webapp allows users with enough privileges to list and change some user attributes through SCIM.
This boils down to calling getUser() et editUser() from this library. This library is not really flexible: it expects to be able to authenticate through UMA.

Do you know any good SCIM library for NodeJS that will work with your plugin?

from scim-for-keycloak.

Captain-P-Goldfish avatar Captain-P-Goldfish commented on August 15, 2024

unfortunately I cannot give you a good advice here, since I am using my own implementation of a SCIM client in my JS applications that is very simplified though.

import {useUserLogin} from "../provider/login-provider";
import {Optional} from "./utils";

export function useScimClient()
{
  
  const auth = useUserLogin();
  
  function createResource(resourcePath, resource, onSuccess, onError)
  {
    auth.accessToken.then(accessToken =>
    {
      fetch(resourcePath, {
        method: "POST",
        headers: {
          'Content-Type': 'application/scim+json',
          'Authorization': 'Bearer ' + accessToken
        },
        body: JSON.stringify(resource)
      }).then(response =>
      {
        let tmpResponse = {
          success: response.status === 201,
          status: response.status,
          resource: response.json()
        };
        if (tmpResponse.success)
        {
          new Optional(onSuccess).ifPresent(func => tmpResponse.resource.then(json => func(json)));
        }
        else
        {
          new Optional(onError).ifPresent(func => tmpResponse.resource.then(json => func(json)));
        }
        return tmpResponse;
      });
    });
  }
  
  function getResource(resourcePath, id, params, onSuccess, onError)
  {
    auth.accessToken.then(accessToken =>
    {
      let searchParams = new Optional(params).map(parameters => "?" + new URLSearchParams(parameters).toString())
                                             .orElse("");
      let url = resourcePath + new Optional(id).map(val => "/" + encodeURIComponent(val)).orElse("") + searchParams;
      
      fetch(url, {
        method: "GET",
        headers: {
          'Authorization': 'Bearer ' + accessToken
        }
      }).then(response =>
      {
        let tmpResponse = {
          success: response.status === 200,
          status: response.status,
          resource: response.json()
        };
        if (tmpResponse.success)
        {
          new Optional(onSuccess).ifPresent(func => tmpResponse.resource.then(json => func(json)));
        }
        else
        {
          new Optional(onError).ifPresent(func => tmpResponse.resource.then(json => func(json)));
        }
        return tmpResponse;
      });
    });
  }
  
  async function listResources({
                                 resourcePath,
                                 startIndex,
                                 count,
                                 filter,
                                 sortBy,
                                 sortOrder,
                                 attributes,
                                 excludedAttributes,
                                 onSuccess,
                                 onError
                               } = {})
  {
    auth.accessToken.then(accessToken =>
    {
      let startIndexParam = new Optional(startIndex).map(val => "startIndex=" + val).orElse(null);
      let countParam = new Optional(count).map(val => "count=" + val).orElse(null);
      let filterParam = new Optional(filter).map(val => "filter=" + encodeURI(val)).orElse(null);
      let sortByParam = new Optional(sortBy).map(val => "sortBy=" + encodeURI(val)).orElse(null);
      let sortOrderParam = new Optional(sortOrder).map(val => "sortOrder=" + val).orElse(null);
      let attributesParam = new Optional(attributes).map(val => "attributes=" + encodeURI(val)).orElse(null);
      let excludedAttributesParam = new Optional(excludedAttributes).map(
        val => "excludedAttributes=" + encodeURI(val)).orElse(null);
      
      let query = Array.of(startIndexParam, countParam, filterParam, sortByParam, sortOrderParam, attributesParam,
        excludedAttributesParam)
                       .filter(val => val != null)
                       .join("&");
      
      query = new Optional(query).filter(val => val.length > 0).map(val => "?" + val).orElse("");
      
      let requestUrl = resourcePath + query;
      
      fetch(requestUrl, {
        method: "GET",
        headers: {
          'Authorization': 'Bearer ' + accessToken
        }
      }).then(response =>
      {
        let tmpResponse = {
          success: response.status === 200,
          status: response.status,
          resource: response.json()
        };
        if (tmpResponse.success)
        {
          new Optional(onSuccess).ifPresent(func => tmpResponse.resource.then(json => func(json)));
        }
        else
        {
          new Optional(onError).ifPresent(func => tmpResponse.resource.then(json => func(json)));
        }
        return tmpResponse;
      });
    });
  }
  
  function updateResource(resourcePath, id, resource, onSuccess, onError)
  {
    auth.accessToken.then(accessToken =>
    {
      fetch(resourcePath + "/" + encodeURIComponent(id), {
        method: "PUT",
        headers: {
          'Content-Type': 'application/scim+json',
          'Authorization': 'Bearer ' + accessToken
        },
        body: JSON.stringify(resource)
      }).then(response =>
      {
        let tmpResponse = {
          success: response.status === 200,
          status: response.status,
          resource: response.json()
        };
        if (tmpResponse.success)
        {
          new Optional(onSuccess).ifPresent(func => tmpResponse.resource.then(json => func(json)));
        }
        else
        {
          new Optional(onError).ifPresent(func => tmpResponse.resource.then(json => func(json)));
        }
        return tmpResponse;
      });
    });
  }
  
  function patchResource(resourcePath, id, patchBody, onSuccess, onError)
  {
    auth.accessToken.then(accessToken =>
    {
      fetch(resourcePath + "/" + encodeURIComponent(id), {
        method: "PATCH",
        headers: {
          'Content-Type': 'application/scim+json',
          'Authorization': 'Bearer ' + accessToken
        },
        body: JSON.stringify(patchBody)
      }).then(response =>
      {
        let tmpResponse = {
          success: response.status === 200,
          status: response.status,
          resource: response.json()
        };
        if (tmpResponse.success)
        {
          new Optional(onSuccess).ifPresent(func => tmpResponse.resource.then(json => func(json)));
        }
        else
        {
          new Optional(onError).ifPresent(func => tmpResponse.resource.then(json => func(json)));
        }
        return tmpResponse;
      });
    });
  }
  
  function deleteResource(resourcePath, id, requestBody, onSuccess, onError)
  {
    auth.accessToken.then(accessToken =>
    {
      fetch(resourcePath + "/" + encodeURIComponent(id), {
        method: "DELETE",
        headers: {
          'Authorization': 'Bearer ' + accessToken
        },
        body: requestBody ? JSON.stringify(requestBody) : null
      }).then(response =>
      {
        let tmpResponse = {
          success: response.status === 204,
          status: response.status,
          resource: response.status === 204 ? undefined : response.json()
        };
        if (tmpResponse.success)
        {
          new Optional(onSuccess).ifPresent(func => func());
        }
        else
        {
          new Optional(onError).ifPresent(func => tmpResponse.resource.then(json => func(json)));
        }
        return tmpResponse;
      });
    });
  }
  
  return {
    createResource: createResource,
    getResource: getResource,
    listResources: listResources,
    updateResource: updateResource,
    patchResource: patchResource,
    deleteResource: deleteResource
  };
}

from scim-for-keycloak.

jacen05 avatar jacen05 commented on August 15, 2024

Thanks, I will try to get it working with my app, and report back probably next week.

from scim-for-keycloak.

jacen05 avatar jacen05 commented on August 15, 2024

In the provided code, I guess that you're using the user access token to authenticate the requests.

  • Does that mean that any authenticated user can do SCIM operations?
  • On a more general perspective, how fine-grained are the authorization in the SCIM server? only client <-> realms? Can I ensure that 2 SCIM clients will only do SCIM operations on their users, and not the other users in the same realm?

from scim-for-keycloak.

Captain-P-Goldfish avatar Captain-P-Goldfish commented on August 15, 2024
  1. There is a yes and a no
  2. The SCIM endpoints are not accessible if no Client in the authorization section was assigned (If the authentication is turned on). And users authenticating over other applications (other Clients) will not be granted access.

Thx. This question pointed out that the documentation on the website on this topic should be brought more to the foreground

Each realm has its own individual security configuration. So the configuration for realm A will have no effect for the configuration on realm B

from scim-for-keycloak.

Captain-P-Goldfish avatar Captain-P-Goldfish commented on August 15, 2024

I will try to create some graphics with enough explanations for the website

from scim-for-keycloak.

jacen05 avatar jacen05 commented on August 15, 2024

Thanks.
So, it seems that there is no way to restrict a SCIM client to manage only "its" users (the roles are bound to a Resource Type, not a resource). This implies that I need to make use of a different realm per business client (a client being in that case an OIDC client + potentially another OIDC client for AzureAD and SCIM if not using the local user DB).
This has some big impacts on our application, so I need to assess the solutions available for multi-tenancy.

from scim-for-keycloak.

Captain-P-Goldfish avatar Captain-P-Goldfish commented on August 15, 2024

Ah I see what you are getting at. This is indeed not possible with this solution. I once had a similiar problem where we wanted to share the same users for different tenants but with different access-rights on these tenants and its resources. The easiest way in my opinion is to use different realms and try to synchronize users between realms. It would probably also be possible to add an create the users in one realm and to add a delegation identity-provider from realm B to realm A so that the users from there are accepted too in realm B.
I actually never tried this its just a theory that might work :-)

from scim-for-keycloak.

jacen05 avatar jacen05 commented on August 15, 2024

Well, the problem for us is more that we wanted the authentication server to be the entry point for all users, and after authentication the user would be redirected to the correct applicative instance by the reverse-proxy. However Keycloak assumes that users will reach the correct realm login URL. So :

  • Either we put all the users in the same realm, so the login URL is the same for everyone (but this is not compatible with different SCIM clients that will not be restricted to only their users). Maybe a plugin like Keycloak Organizations can help.
  • Either we go multi-realm, by developping a custom component that will redirect to the correct realm, or using a plugin like Keycloak Multi-Tenancy.

from scim-for-keycloak.

jacen05 avatar jacen05 commented on August 15, 2024

FYI, we made our own implementation of NodeJS SCIM library (only the few functions needed). This work great!
Now we'll have to find a solution for the mutli-tenancy problem.
Anyway this has nothing to do with UMA compatibility, so you can close the issue if you want, as we do not need it anymore, or keep it open if you think this feature can be interesting for the project.

from scim-for-keycloak.

Related Issues (20)

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.