How to add custom org and role in grafana with AWS cognito as oauth provider
Srikanth Kyatham

Srikanth Kyatham @srikanthkyatham

About: Software engineer interested in building stuff

Location:
Oulu, Finland
Joined:
Jun 21, 2020

How to add custom org and role in grafana with AWS cognito as oauth provider

Publish Date: Mar 31
1 0

AWS Cognito + Grafana

Idea

  • To be able to configure the user to a given organisation.
  • In AWS cognito there are users and groups.
  • If a user is added to a group based on the group which would be of format (GRAFANA_{orgName}_{role}), where the orgName is organisation name in grafana, then user to would be added to respective organisation on successful oauth sign in.
  • In order that to happen, we need to add pre token generation lambda, which would add custom claims to the id token.
  • Grafana needs to configure role and org attributes to in order extract the role and orgs and assign the user to correct organisations with the correct role.

Grafana role and org attributes

  • role_attribute_path - the documentation is quite straight forward, in our case we want to extract the role from custom_role in the id_token response.
  • org mapping - this was a bit tricky, we need to configure two attributes
    • org_attribute_path
    • org_mapping
  • in our case we want the users to be assigned to organisations found in the org_attribute_path. In order that to happen
    • org_mapping needs to be updated, when an organisation is created in grafana. Meaning
    • Lets say that the org_mapping is
org_mapping = org1:2:Viewer
Enter fullscreen mode Exit fullscreen mode

Now we have created a new organisation org2, then the org_mapping needs to be updated as

org_mapping = org1:2:Viewer,org2:3:Viewer
Enter fullscreen mode Exit fullscreen mode

where
org1 is the organisation name
2 is the organisation id
You can find these details in the grafana>Administration>general>organisations

Grafana configuration

  • The following attributes needs to updated
    • role, org mapping attributes
    • clientId
    • clientSecret
    • authUrl
    • tokenUrl
    • apiUrl
  • Domain url could be fetched from AWS cognito like shown in the figure

Image description
in custom.ini, *.ini

[log]
level = debug
[users]
auto_assign_org = true
[auth.basic_auth]
enabled = debug
[auth.generic_oauth]
client_id = <client_id>
client_secret = <client_secret>
scopes = <scopes>
auth_url = <domain_url>/oauth2/authorize
token_url = <domain_url>/oauth2/token
api_url = <domain_url>/oauth2/userInfo 
signout_redirect_url = <domain_url>/logout?client_id=<client_id>&logout_uri=<logout_url>
role_attribute_path = ("custom_role" || contains([*], 'ADMIN') && 'Admin') || ("custom_role" || contains([*], 'EDITOR') && 'Editor') || 'Viewer'
role_attribute_strict = true
org_attribute_path = "custom_orgs"
Enter fullscreen mode Exit fullscreen mode

if you are using docker-compose then the environment variables would be.

The variables

example docker-compose.yml

services:
  grafana:
    image: grafana/grafana-oss
    container_name: grafana
    restart: unless-stopped
    environment:
      - GF_AUTH_GENERIC_OAUTH_ENABLED=true
      - GF_AUTH_GENERIC_OAUTH_CLIENT_ID=<client_id>
      - GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET=<client_secret>
      - GF_AUTH_GENERIC_OAUTH_AUTH_URL=<domain_url>/oauth2/authorize
      - GF_AUTH_GENERIC_OAUTH_TOKEN_URL=<domain_url>/oauth2/token
      - GF_AUTH_GENERIC_OAUTH_API_URL=<domain_url>/oauth2/userInfo
      - GF_AUTH_GENERIC_OAUTH_API_URL=<domain_url>/logout?client_id=<client_id>&logout_uri=<logout_url>
      - GF_AUTH_GENERIC_OAUTH_SCOPES=<scopes>
      - GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH=("custom_role" || contains([*], 'ADMIN') && 'Admin') || ("custom_role" || contains([*], 'EDITOR') && 'Editor') || 'Viewer'
      - GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_STRICT=true
      - GF_AUTH_GENERIC_OAUTH_ORG_ATTRIBUTE_PATH="custom_orgs"
      - GF_LOG_LEVEL=debug
      - GF_USERS_AUTO_ASSIGN_ORG=true
      - GF_AUTH_BASIC_ENABLED=true
Enter fullscreen mode Exit fullscreen mode

AWS Lambda

  • AWS lambda - Purpose is to populate the custom claims, which in our case are the organisation (custom_orgs) and role of the user (custom_role), which would be extract in the grafana upon succesfull oauth
    • In order to add pre auth token lambda we need to click on the extensions in the cognito user pool

Image description

  • Add pre token generation lambda options should be as shown in the figure
    • Trigger type - Authentication
    • Authentication - Pre token generation trigger
    • Trigger event version - Basic features + access token customization atleast
    • Select the lambda to be added

Image description

  • code for the lambda is
interface Event {
    request: {
        groupConfiguration: {
            groupsToOverride: string[]
        }
    }
    response: {
        claimsAndScopeOverrideDetails?: {
            idTokenGeneration?: {
                claimsToAddOrOverride?: {
                    custom_orgs: string[]
                    custom_role: string | null
                }
            }
        }
    }
}

export async function handler(event: Event) {
    // Log the event for debugging
    const cognitoGroups = event.request.groupConfiguration.groupsToOverride
    // Extract groups from Cognite or other attribute sources
    // For example, if you have groups stored in a cognito:groups attribute
    const groups = cognitoGroups

    // Filter for Grafana groups and format them
    const grafanaGroups = groups.filter((group: string) => group.startsWith("GRAFANA_"))

    const customOrgs = grafanaGroups.map((group: string) => group.split("_")[1])

    const customRole =
        grafanaGroups.length > 0 && grafanaGroups[0].split("_").length > 2 ? grafanaGroups[0].split("_")[2] : null



    // Add the custom claim to the ID token
    event.response = {
        claimsAndScopeOverrideDetails: {
            idTokenGeneration: {
                claimsToAddOrOverride: {
                    custom_org: customOrgs,
                    custom_role: customRole,
                },
            },
        },
    }

    return event
}
Enter fullscreen mode Exit fullscreen mode
  • When the user is created in the AWS Cognito
    • Create AWS cognito user
    • Create AWS cognito group GRAFANA_{ORGNAME}_{ROLE}
    • Assign the cogito user to the group
    • Create the grafana org - POST /org
const grafanaUrl = "http://localhost:3000"

const orgsEndPoint = "/api/orgs"
const orgUrl = grafanaUrl + orgsEndPoint
const clientId = "you_client_id"
const clientSecret = "you_client_secret"

function basicAuthHeader(user, pwd) {
    const combo = `${user}:${pwd}`
    const base64String = Buffer.from(combo).toString('base64');
    const header = `Basic ${base64String}`
    return header
}

function commonHeaders() {
    const headers = {
        'authorization': basicAuthHeader(grafanaUser, grafanaPwd)
    }
    return headers
}

const commonHeader = commonHeaders()


async function createOrg(orgName) {
    const body = { "name": orgName }
    const url = orgUrl
    const response = await fetch(url, {
        method: 'POST',
        headers: {
            ...commonHeader,
            'User-Agent': 'undici-stream-example',
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(body),
    });
    const data = await response.json()
    return data
}
Enter fullscreen mode Exit fullscreen mode
  • Update the sso-settings/generic_oauth, orgMapping
async function getSsoSettings() {
    const url = ""
    const options = { method: 'GET', headers: commonHeader }
    const response = await fetch(url, options);

    const data = await response.json();
    return data
}

async function updateSsoSettings(body) {
    const url = `${grafanaUrl}/api/v1/sso-settings/generic_oauth`
    const response = await fetch(url, {
        method: 'PUT',
        headers: {
            ...commonHeader,
            'User-Agent': 'undici-stream-example',
            'Content-Type': 'application/json',
        },
        body: JSON.stringify(body),
    });
    const data = await response.json()
    return data
}

async function updateOrgMapping(newOrgName, newOrgId) {
   const ssoSettings = await getSsoSettings()
   const genericProvider = ssoSettings.filter(setting => setting["provider"] === "generic_oauth")
   const genericOauthSettings = genericProvider["settings"]
   const orgMapping = JSON.parse(genericOauthSettings["orgMapping"])
   const updatedOrgMapping = [...orgMapping, `${newOrgName}:${newOrgId}:Viewer`]
   const putBody = {
    "settings": {
        "clientId": clientId,
        "authUrl": `${DomainUrl}/oauth2/authorize`,
        "tokenUrl": `${DomainUrl}/oauth2/token`,
        "orgMapping": updatedOrgMapping
      }
    }
   await updateSsoSettings(putBody)
}
Enter fullscreen mode Exit fullscreen mode

Comments 0 total

    Add comment