- read

Authentication and Authorization of Go Rest Api’s using an open-source IAM called Keycloak

Allu Sai Prudhvi 67

Architecture

In this blog, you will see how different users can access APIs based on the scope assigned to them. With the help of Keycloak, a third-party open-source Identity, and Access Management server, we create users and assign roles to them. A JWT token is generated when the user hits the Keycloak server with his credentials. This JWT access_token is passed in the headers of the HTTP request to the APIs, In the Golang project, the access_token is verified against the Keycloak server. If the signature is valid then the scope’s obtained from the access_token is validated and the user is authorized accordingly.

Let’s name Our Go Microservice as DemoService. In this service, we create two APIs, one is the getPetsSearch API which returns pets with the name searched. Other is the getPetDetails API which returns the details of the pet. Now, we try to authenticate these two APIs with the users created in Keycloak.

Keycloak Installation and steps to generate a token from scratch:

Keycloak is an Open Source Identity and Access Management that adds authentication to the applications and secure services with minimum fuss. With the help of the Keycloak, it is easy to create, manage and assign scopes to the users.

Installation:

docker run -d -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin --name keycloak jboss/keycloak:4.1.0.Final

After running the above command, log in to the Keycloak admin console with the username and password given while creating the Keycloak container and those are the admin credentials

http://localhost:8080/auth/admin/master/console

Now, I'll illustrate with an example from scratch on how to do create a realm, user, groups, clients, roles, and so on.

  1. Realm creation :
    => Realms manages a set of users, credentials, groups, clients, and roles.
    => Each Realm is isolated from one other. users of that realm can only log into that realm.

By default, A admin realm called Master is created. The credentials for this realm are given while creating the container and this admin has access to all the realms, so he can create realms and manage them as well.

Now, we create a realm called DEMOREALM

CLICK ADD REALM TO CREATE REALM

Currently, we logged in as an admin user of the master realm, so he can see all the realms. If someone working explicitly on an isolated realm, ie. DEMORELAM, it’s not advisable to give the master admin credentials to that user. Instead of that, you can create a user under the DEMOREALM and assign the role realm-admin of the realm-management client to the user. We called that user an Admin of that realm. Let’s see how to create :

step 1: Add a user named admin-demo:

Here, we are creating a user with username admin-demo and with other values as shown in the below screenshot. You can hover on the question mark to understand more about the fields.

After creating the user, under the admin-user, credentials tab, you can create the temporary password for the user. Now, try to access the account page using the below URL:

http://localhost/auth/realms/DEMOREALM/account/

It will ask you to enter the credentials. So, please enter the credentials given for the admin-demo user of DEMOREALM.

After logging in, it will ask you to update the password as we are logging in for the first time.

step 2. Now, try to open the DEMO-REALM console with the below-given URL and enter the credentials of the admin-demo of the DEMO-REALM user.

http://10.66.29.167:9999/auth/admin/DEMOREALM/console/
user doesn't have a realm-admin role to view the console

So, we should give the realm-admin role of the realm-management client to the user. Now, again login into the master realm using master realm credentials, and then under DEMOREALM, go to the admin-demo user. Inside admin-demo user, in the Role-Mapping section, select realm-management options under client roles and select realm-admin under available roles and click add selected. After clicking that, you can see realm-admin role is added under Assigned roles and all other roles along with the realm-admin role are added under effective roles, that’s because we are giving admin access to the user, which means all other roles comes under the realm-admin role.

Now, try again to open the DEMO-REALM console with the below given URL and enter the credentials of the admin-demo of the DEMO-REALM user, you will be navigated to the realms screen. Only DEMO-REALM is seen unlike for master admin users. Click the DEMO-REALM, you will be navigated to the DEMO-REALM console. From, now on-wards, we can work independently as an admin of DEMO-REALM instead of an admin of the master realm who has complete access to all realms.

2. Create Client :
=> Clients are the entities that request the Keycloak server to authenticate users
=> Client is an application that requests an access token so that it can invoke other services on behalf of the authenticated user.

For example, realm Management is a Client, and the actions that need to be performed to manage the realm are called roles, i.e. view-users, query-users, and so on. Those roles are attached to the realm management client.

We should authenticate and authorize users for our Go Microservice API's. Let’s call our Golang microservice project as DemoService. Here DemoService is the client that has APIs for which users need to be authenticated. let’s create a client called DemoService. Under clients, you can click create button to create a client.

after clicking the save button, the DemoServiceClient will be created, under settings, select the access type field as confidential to initiate login protocol with credentials and also enter some URL to which user need to redirect after successful login in Valid Redirect URIs field.

After clicking save, a new credentials tab is created right to settings, where you can find client-secret which is useful while the user request’s an access token.

3. Create Roles :
=> Roles identify a type or category of a user.
=> one user can have multiple roles.
=> roles are assigned to users or groups.
=> ex : pets-search => to access pets search api
=> roles can be composite. i.e pets-admin => pets-search + pet-details

let's assume that the pets-search role is to access getPetsSearch API and the pet-details role is to access getPetDetails API. Now create these two roles under DemoServiceClient. So, when the user hits getPetsSearch API, he will be only authorized if the pets-search role is attached to him (this logic should be implemented in the DemoService project).

let us also create pet-admin role which is a composite role =>pets-search + pet-details. so, whoever has this role can access both getPetsSearch API and getPetDetails API. Here, while creating this pets-admin Composite Role should be enabled and under client roles, select DemoServiceClient. After selecting DemoServiceClient, you will be able to see the roles created under DemoServiceClient, i.e.,pets-search and pet-details. So, click add selected on both roles.

4. Create Users:

Created two users with their names and password as user001 and user002.

let’s attach some roles to the users created. Here, I’m attaching the pets-admin role to user001 and the pets-search role to user002.

adding the pets-admin role to user001
adding the pets-search role to user002

5. Generate Tokens:
=>id_token: contains info about the user entity
=>access_token: this is to authorize the third-party services
=>refresh_token: this is to get a new access_token when it got expired
=>if you want to see the claims and roles of access_token in the Keycloak server itself (generally we copy the token and see in jwt.io), then under a client,

  1. Go to client_scopes under DemoServiceClient.
  2. Click on Evaluate and under Evaluate search for the user whom you want to generate the token and click below Evaluate button which is in blue color.
  3. After Evaluating, you can see the Effective Protocol Mappers, Effective Role Mappers, and Generated Access token. You can click on Effective Protocol Mappers and Effective Role Mappers, to view the protocols and roles attached to the users.
  4. Click on the Generated Access token, to view the payload of the JWT Access token. To know more about JWT token structure, click here.

In the payload, you can see the following roles attached to the user001. These are the roles that we have attached.

"DemoServiceClient": {
"roles": [
"pets-admin",
"pet-details",
"pets-search"
]
}

6. GET ACCESS TOKEN API :

The following curl is for getting the token for the user001. Here client_id is our DemoServiceClient and client-secret is obtained from the credentials tab under DemoServiceClient. username and password are the credentials of the user who request a JWT access token.

curl --location --request POST 'http://localhost:8080/auth/realms/GOMMT-B2B/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=DemoServiceClient' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'username=user001' \
--data-urlencode 'password=user001' \
--data-urlencode 'scope=openid' \
--data-urlencode 'client_secret=e2295e0d-4289-473c-a350-cbdcec2a0eac'

Response:

{ "access_token": "eyJhbGciOiJSUzI1NiIsIn...", "expires_in": 300, "refresh_expires_in": 1800, "refresh_token": "eyJhbGciOiJSUzI1NiIsIn....", "token_type": "bearer", "id_token": "eyJhbGciOiJSUzI1NiIsIn...", "not-before-policy": 0, "session_state": "a8174974-c5c6-48f3-b930-5fe0d011b396", "scope": "openid profile email" }

7. Few more Keycloak terms :

Protocol Mappers :
=>these are useful in adding data to the tokens
=>under the client section in Mappers, you can create or built-in options through which you can add data to the tokens such as claims roles, etc.

Groups :
=> set of users.
=> a user can belong to multiple groups.
=> we can assign roles to a group and those roles will be assigned to all the members of the group
=> there is something called default groups, you create a group and save it under default group, whenever a user is added by default he will
assigned to that default group.

Verification of Access Token and user scope check at the API level

To add authentication, lets’ first create a sample go project with two mock APIs.

  1. getPetDetails Api: This API returns details of the when searched with a pet name
  2. getPetsSearch Api: This API returns all the breed’s of a pet when searched with a pet type

When user hits the route ,“/demoservice/getPetDetails”, it triggers getPetDetails function. Here, we add a middleware function that verifies the access token before calling the getPetDetails.

router.GET("/demoservice/getPetDetails", middleware.IsAuthorizedJWT(getPetDetails, "pet-details"))

IsAuthorizedJWT function expects the handler and the role that corresponds to the API as arguments. The function first verifies the token against the Keycloak server and then checks the scopes of the user against the role passed.

func IsAuthorizedJWT(h httprouter.Handle, role string) httprouter.Handle {return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
if(access_token verification && check role against the roles mapped with the user){
h(w,r,ps)
}
authorizationfailed()
}

Use the OpenID Connect discovery mechanism to construct a Provider. The issuer is the URL identifier for the service. Here, the issuer is RealmConfigURL=”http://localhost:8080/auth/realms/DEMOREALM”

rawAccessToken := r.Header.Get("Authorization")tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{
Timeout: time.Duration(6000) * time.Second,
Transport: tr,
}
ctx := oidc.ClientContext(context.Background(), client)
provider, _ := oidc.NewProvider(ctx, RealmConfigURL)
if err !=nil{
authorizationFailed()
}

Define a verifier with config values like ClientID, SkipExpiryCheck, SkipClientIDCheck, and others based on the level of verification needed. Verify the rawAccessToken by passing it as an argument in verify function. If there is any error returned while verifying the token, send the response status as StatusUnauthorized. Here, our clientID is DemoServiceClient

clientID :="DemoServiceClient"
oidcConfig := &oidc.Config{
ClientID: clientID,
}
verifier := provider.Verifier(oidcConfig)
idToken, err := verifier.Verify(ctx, rawAccessToken)
if err != nil {
authorizationFailed("authorization failed ,verifying the token")
return
}

After successful verification of token, get the claims from the idToken of type (*oidc.IDToken). The Claims field inside idToken is of type JSON. So, construct the Claims struct first and then UnMarshal the JSON. Define Claims struct with the help of access_token JSON generated in the previous Keycloak section.

The claims component of jwt contains many fields, we need only the roles of DemoServiceClient.

type Claims struct {
ResourceAccess client `json:"resource_access,omitempty"`
JTI string `json:"jti,omitempty"`
}
type client struct {
DemoServiceClient clientRoles `json:"DemoServiceClient,omitempty"`
}
type clientRoles struct {
Roles []string `json:"roles,omitempty"`
}
var IDTokenClaims Claims // ID Token payload is just JSON.
if err := idToken.Claims(&IDTokenClaims); err != nil {
authorisationFailed("claims : "+err.Error(), w, r)
return
}
fmt.Println(IDTokenClaims)

Now, user_access_roles contained all the roles attached to the user for the DemoServiceClient. We should check role(passed argument) within the user_access_roles array, if the role is present in the array then the user is allowed to access the API, so we forward the request params to the HTTP handler


user_access_roles:=IDTokenClaims.ResourceAccess.DemoServiceClient.Roles
for _, b := range user_access_roles {
if b == role {
h(w, r, ps)
return
}
}

Here, you can find the whole code wrapped that is discussed above.

Test the api’s :

  1. getPetsSearch :

Get the Authorization header for the users, from the getAccessToken api given above in the Keycloak section.

curl --location --request GET 'http://localhost:3333/demoservice/getPetsSearch?pet_type=dog' \
--header 'Authorization: eyJhbGci.....'

For user001,user002, get the access_token and pass it in the authorization header to the above api. Both authentication and authorization will happen successfully, as both the users have pets-search role attached to them.

["Golden Retreiver","Afador","pariah","Labrador","German Shepherd"]

2. getPetDetails :

Get the Authorization header for the users, from the getAccessToken api given above in the Keycloak section.

curl --location --request GET 'http://localhost:3333/demoservice/getPetDetails?pet_name=pinku&' \
--header 'Authorization: eyJhbGciOiJSUzI...'

For user001, get the access_token and pass it in the authorization header to the above api. Both authentication and authorization will happen successfully because the user001 has a pet-details role attached to him.

But for user002, he will receive the authorization failed response with 401 as status code, that’s because the pet-details role is not attached to him. He receives the following response.

{
"status": "FAILED",
"httpCode": 401,
"message": "user not allowed to access this api"
}

Project Link: https://gitlab.com/ALLU999/gomicroservicewithauth