Adding new permission to shared resources with keycloak REST API

January 16, 2023 – Petri Lammi

What does it do, this Keycloak thing?

Dear seasoned keycloacker, as you probably know, keycloak is a stable, scalable, programmable and otherwise killer platform to centralize all your identity, authentication and authorization needs. It supports standard protocols such as:

  • OpenID Connect
  • OAuth 2.0
  • SAML
  • Kerberos

If you are on the business side of things and worry about support, continuity, standardization, compliance, SLAs and stuff (you should), Red Hat Single Sign-On is the supported version of Keycloak, that you can get a subscription for, and be done with all your worries. For details, contact Us or Red Hat.

What is it good for?

You might use it for:

  • Single-Sign On with OTP/MFA and all WebAuthn goodies
  • Centralized management of identities
  • Identity brokering - authenticate with other OpenID Connect or SAML 2.0 Identity Providers
  • User federation (using already existing identities from backends like Active Directory or Red Hat Identity Management server or LDAP)
  • Authentication and authorization to your Uber awesome hit application with unreasonable requirements in a scalable and standards-based way, while still maintaining your sanity
  • Use fine-grained authorization and UMA for your awesome new business idea that requires users to give permissions to other users.
  • Centrally protect your precious APIs
  • Some esoteric and original purpose

With all the features and power, comes the cost of complexity, but it has real design to keep this complexity in shape. Keycloak has a pretty WebUI (at least the latest versions) to make you handle tasks that are suitable for a human with ease.

Assuming things

Every technical blog will be obsolete tomorrow. Without specifying what is assumed, people might waste their precious time and feel bad. We're nice and happy people, we don't want that. Therefore we assume that:

  • You have some basic understanding of REST in general
  • You paid some attention in your Ruby class
  • You like (or don't hate) object oriented approach to things
  • You have a test Keycloak instance somewere around you
  • Your Keycloak version is 15.0.2
  • You know your ways with Tcpdump or Wireshark
  • You use Emacs as your operating environment. Nah, just kidding. Just use that vscode.

From clicking to code

For things that we humans would really not like perform, usually repetitive tasks, they invented programming languages and REST APIs. Some then went farther and used programming languages to invent IaC tools to maintain a desired state for all your fleet, and some, ahem, then automate all kinds of things with those tools (Terraform, Ansible, Puppet anyone?) Talking to the API with a programming language, however, is what we will cover here. If you are the infamous clicking operator in your company, and actually enjoy your work, perhaps this is not for you. 

The language we use here is Ruby, and the gems we need are HTTParty and json. Ruby is a pretty language and I like pretty languages. The goal is to add a new permission, a scope-based permission to be precise, to already existing shared resources. As noted, this is not very enjoyable with thousands of resources in the WebUI, unless you took overdose of steroids and built inhuman clicking muscles, or learned how to do RPA as an ugly workaround. Yes, we can also do robot for you, if you absolutely require. However Keycloak is all about APIs, so let's leave them UIs alone. 

Set it ready for some REST talk

First we need to do some set up. In your master realm, admin-cli client, enable service accounts and authorization and set client access type to confidential:

Setting up

Then under Credentials tab, set Client authenticator to Client id and secret and copy the Secret:

Now we have the capability to talk to the thing. In production thou shalt not use the master realm and admin-cli client. Thou shalt disable them. But we’re working with a test instance here, that we can re-create in an instant, right? And we want all the power and torque. 

Let's REST

Some class action

Ok, now for the code. Let get our class started:

#!/usr/bin/env ruby

require 'httparty'
require 'json'
require './config'

class KeycloakClient
  include HTTParty

  attr_accessor :token

This is pretty obvious: we get the configuration values by using a Config class from the file config.rb. That class reads the values from an ini-style file, that we initialize it with. 

Let's initialize

First let’s initialize the wanna-be instance with the server and authentication details, and get a token. Using an inifile makes it easy to DRY and switch between instances:

def initialize(server_url, grant_type, client_id, client_secret)
    @server_url = server_url
    @auth = { grant_type: grant_type, client_id: client_id, client_secret: client_secret }
    @token = get_token
  end

Everyone needs a token

Here’s the method to obtain and store the token:

def get_token
    response = self.class.post("#{@server_url}/auth/realms/master/protocol/openid-connect/token",
                               body: {
                                 grant_type: @auth[:grant_type],
                                 client_id: @auth[:client_id],
                                 client_secret: @auth[:client_secret]
                               },
                                 headers: { 'Content-Type' => 'application/x-www-form-urlencoded' })
    response.parsed_response['access_token']
  end

Now, what do we actually need to do?

Getting a token is nice, but not very interesting. For meaningful operations, we need to do a couple of things:

  1. Convert our target client name to an internal id
  2. Search and get list of existing resources based on type
  3. Retrieve the hash of the scope that we want to add to the resources
  4. Retrieve a resource hash based on name 
  5. Update the current resources

What's the client's id?

After instantiating the class and having gotten the token, we need the target client id, where the resources are located. Here’s a method to get that based on it’s name:

 def get_client_id_by_name(realm, client_name)
    response = HTTParty.get("#{@server_url}/auth/admin/realms/#{realm}/clients",
                              headers: { 'Authorization' => "Bearer #{@token}" })
    client = JSON.parse(response.body).find { |c| c["clientId"] == client_name }
    return client["id"] if client
  end

What are the existing resources?

With this method, we build an array of our existing resources of certain type (type is a completely arbitrary string).

# returns array
  def find_by_type(array, type)
     value = array.select { |hash| hash["type"] == type }
   end

# returns array
   def get_resources_by_type(realm, client_id, type)
     response = HTTParty.get("#{@server_url}/auth/admin/realms/#{realm}/clients/#{client_id}/authz/resource-server/resource?deep=false&first=0&max=1000",
                             headers: { 'Authorization' => "Bearer #{@token}" })
     matches = find_by_type(response.parsed_response, type)
   end

What does the scope look like as a hash?

For building the final payload, we need to retrieve the new scope as a hash:

# returns hash
  def get_scope_hash_by_name(realm, client_id, scope_name)
    response = HTTParty.get("#{@server_url}/auth/admin/realms/#{realm}/clients/#{client_id}/authz/resource-server/scope?deep=false&first=0&max=1000",
                              headers: { 'Authorization' => "Bearer #{@token}" })
    name = JSON.parse(response.body).find { |c| c['name'] == scope_name }
    return name if name
  end

How do I feed the server?

And finally we need a method to actually shoot our payload. We also end the class.

def update_resource(realm, client_id, payload, resource_id)
    response = HTTParty.put("#{@server_url}/auth/admin/realms/#{realm}/clients/#{client_id}/authz/resource-server/resource/#{resource_id}",
                            headers: {
                              "Authorization" => "Bearer #{@token}",
                              "Content-Type" => "application/json"
                            },
                            body: payload.to_json
                           )
  end
end # class ends

Where to get the parameters?

To use these methods, some parameters need to be to initialized. As mentioned, I use a config class that gets it values from an ini file

config = Config.new(’./config.ini')
realm = config.keycloak_target_realm
client_name = config.keycloak_target_client
server_url = config.keycloak_server_url
username = config.keycloak_username
password = config.keycloak_password
grant_type = config.keycloak_grant_type
client_id =  config.keycloak_client_id
client_secret = config.keycloak_client_secret
target_resource_type = config.keycloak_target_resource_type
new_scope_name = config.keycloak_new_scope_name

Create a new client instance

After these we create a shiny new instance:

client = KeycloakClient.new(server_url, grant_type, client_id, client_secret)

Talk to the right client

Get the target client id:

target_client_id = client.get_client_id_by_name(realm, client_name)

Collect the existing resources

Build an array of existing resources that match the type you want:

resources = client.get_resources_by_type(realm, target_client_id, type))

What does the scope look like as a hash?

Get the new scope as a hash:

new_scope_hash = client.get_scope_hash_by_name(realm, target_client_id, new_scope_name)

Feeding time

Now we can finally do some real work.

We iterate over our existing shared resources and build an acceptable payload. Then load it to the server. This payload will associate the new scope with the resources of the type you wanted:

resources.each do |resource|
  payload = {}
  resource_name = resource['name']
  h = client.get_resource_hash_by_name(realm, target_client_id, resource_name)
  if h.has_key?('scopes') and h['scopes'].any? { |e| e['name'] == new_scope_name }
    puts("resource '#{resource['name']}' already has the scope '#{new_scope_name}', skipping...")
    next
  end
  payload['name'] = h['name']
  payload['type'] = h['type']
  payload['owner'] = h['owner']
  payload['ownerManagedAccess'] = h['ownerManagedAccess']
  payload['displayName'] = h['displayName']
  payload['_id'] = h['_id']
  payload['uris'] = h['uris']
  current_scopes = h['scopes'] || []
  updated_scopes = current_scopes + [new_scope_hash]
  payload['scopes'] = updated_scopes
  print("updating resource '#{resource_name}'...")
  response = client.update_resource(realm, target_client_id, payload, h['_id'])
  if response.code == 200 || response.code == 201 || response.code == 204
    puts "ok"
  else
    puts "failed, got #{response.message}"
  end
end

Good luck!

Run it and (hopefully) enjoy your new scope-based permission to further authorize your resources (or theirs, with UMA). Of course you can improve from here. This was meant to be a short demonstration of how to do REST to manage keycloak Authorization resources. There are many other ways to use keycloak APIs for all kinds of things, perhaps to build killer puppet types and providers, Ansible modules, or Terraform providers. That’s what we pretty much do here: automate anything that moves and make stuff to behave, for fun and profit.

The methods here are mostly a result of tcpdumping the WebUI traffic and experimenting. The documentation does not always cover all the necessary things. I’m sure there are other, perhaps funnier and/or more elegant ways to do this. If you know about cool Keycloak stuff, tell us, if you don't, don't. The following might be useful with your RESTing practices with Keycloak. What it does is shamelessly left as an internet search exercise for the reader:

tcpdump -vvv -ni lo port 8080 and 'tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x50555420' or 'tcp[((tcp[12:1] & 0xf0) >> 2):4] = 0x504F5354'

Want to talk to an expert?

If you want to reach us, just send us a message or book a free call!
Categories

Tags

#aad #Access #acl #alertmanager #ansible #ansible module development #Apache #API #augeas #authentication #authorization #authz #automation #automatization #aws #azure #backup #bash #bitbucket #buildbot #cache #centos #cloud #cloud-init #cloudflare #cloudfront #cluster #connectionsJpa #control repo #custom fact #database #debian #devops #digital sovereignty #DNS #docker #domain mode #duplo #edenred #ejabberd #email #encryption #endpoints #erb #europe #eyaml #fabric #facter #facts #fargate #fedora #file #finnish #foreman #freeipa #git #github #gitlab #gnome #google #grafana #hammer #hiera #HTTparty #IAM #IDM #import #infinispan #Infrastructure as Code #ipmi #irc #jboss #jdk #jenkins #JMESPath #json #kanban #keycloak #librarian-puppet #librenms #linkedin #Linux #Location #loop #marketing #mautic #Mellon #mfa #microsoft #monitoring #mysql #nagios #network-manager #oauth #oauth2 #office365 #open source #openvpn #oxygen #packer #paranormal #pdk #people #php #pkcs7 #pomodoro #Powershell #preseed #presentation #profiles #prometheus #provisioning #puppet #puppet-bolt #puppet-litmus #puppetboard #puppetdb #Puppetfile #puppetserver #puppet types and providers #pxeboot #qemu #quality #r10k #recruitment #redirect #REST #Restrict #Reverse Proxy #robotframework #roles #rspec #ruby #SAML #sem #shell #showsql #snmp #snmpd #software developement #spam #ssh #sso #standardization #systemd #systemd-resolved #teams #terraform #ubuntu #user-data #vagrant #vanity awards #variable #vim #virtualbox #visualstudio #webdevelopment #wildfly #Windows #wireguard #wordpress #workflow #x11 #xmpp #zimbra
We are
 Puppeteers
menucross-circle