Automated client registration method 1

IBM Cloud Pak® for Multicloud Management uses OpenID Connect (OIDC) to allow integrated workloads to use integrated authentication in IBM Cloud Pak for Multicloud Management. You can obtain the OAuth secret after you deploy a Helm chart.

You can register with the OIDC and obtain the secret after the Helm chart is deployed. Registration during a deployment is not recommended because it requires the chart installer or the application service account to have cluster administrator authority and requires cross-namespace access.

As part of the Helm chart deployment, a container, which includes the logic that is used to register with OIDC, is deployed.

Following is a sample users-config.yaml file:

{{/*********************************************************** {COPYRIGHT-TOP} ****
* Licensed Materials - Property of IBM
*
* "Restricted Materials of IBM"
*
*  5737-H89, 5737-H64
*
* © Copyright IBM Corp. 2015, 2018  All Rights Reserved.
*
* US Government Users Restricted Rights - Use, duplication, or
* disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
********************************************************* {COPYRIGHT-END} ****/}}
{{- $compName := "cem-users" -}}
{{- include "sch.config.init" (list . "cem.sch.chart.config.values") -}}
{{- $configMapName := include "sch.names.fullCompName" (list . $compName) -}}
kind: ConfigMap
metadata:
  name: {{ $configMapName }}
  namespace: {{ .Release.Namespace }}
  labels:
{{ include "sch.metadata.labels.standard" (list .) | indent 4 }}
    origin: helm-cem
apiVersion: v1
data:
  oidcPayload.json: |-
    {
      "token_endpoint_auth_method":"client_secret_basic",
      "client_id": "${client_id}",
      "client_secret": "${client_secret}",
      "scope":"openid profile email",
      "grant_types":[
        "authorization_code",
        "client_credentials",
        "password",
        "implicit",
        "refresh_token",
        "urn:ietf:params:oauth:grant-type:jwt-bearer"
      ],
      "response_types":[
        "code",
        "token",
        "id_token token"
      ],
      "application_type":"web",
      "subject_type":"public",
      "post_logout_redirect_uris":[
        "https://{{ .Values.global.masterIP }}:{{ .Values.global.masterPort }}/console/logout"
      ],
      "preauthorized_scope":"openid profile email general",
      "introspect_tokens":true,
      "trusted_uri_prefixes":[
        "https://{{ .Values.global.masterIP }}:{{ .Values.global.masterPort }}"
      ],
      "redirect_uris":[
         "https://{{ .Values.global.masterIP }}:{{ .Values.global.masterPort }}/auth/liberty/callback",
         "https://{{ .Values.global.ingress.domain }}/{{ .Values.global.ingress.prefix }}",
         "https://{{ .Values.global.ingress.domain }}/{{ .Values.global.ingress.prefix }}users/api/authprovider/v1/icp/return"
      ]
    }
  oidc_reg.sh: |-
    #!/bin/bash
    #validate no: of args
    if [[ "$1" == "?" ]]; then
      echo "USAGE: oidc_reg.sh { OIDC_CREDENTIALS }"
      echo
      echo "Run as cluster admin from your master node to register credentials for Cloud App Management."
      echo
      exit 1
    else
      echo "Registering IBM Cloud Event Management identity ..."
    fi
    OIDC_CREDENTIALS="$1"
    OIDC_CREDENTIALS="$(base64 --decode <<< $OIDC_CREDENTIALS)"
    if [ -z "$OIDC_CREDENTIALS" ] ; then
     echo "✖ Error: OIDC_CREDENTIALS not valid!" >&2;
       echo
       exit 1
    fi
    echo
    #replace variables in OIDC_PAYLOAD and write to a temp file
    sed -e "s/\${client_id}/$AUTH_ICP_CLIENT_ID/" -e "s/\${client_secret}/$AUTH_ICP_CLIENT_SECRET/" /etc/oidc/oidcPayload.json > /tmp/OIDC_PAYLOAD_TEMP
    #Check registration
    echo "Checking registration..."
    REG_CHECK_RESULT="$(curl -k -X GET -u "Authorization: Bearer ${ACCESS_TOKEN}" -H "Content-Type: application/json" https://{{ .Values.global.masterIP }}:8443/idprovider/v1/auth/registration/$AUTH_ICP_CLIENT_ID)"
    echo
    #echo REG_CHECK_RESULT:
    #echo $REG_CHECK_RESULT
    #echo
    if [[ $REG_CHECK_RESULT = *"access_denied"* ]]; then
    echo "✖ Authentication failed, must be a admin"
      echo
    fi
    if [[ $REG_CHECK_RESULT = *"client_id_issued_at"* ]]; then
    echo "✔ Client exists..."
      #Update registration
      echo "Updating registration..."
      curl -k -X PUT -u "Authorization: Bearer ${ACCESS_TOKEN}" -H "Content-Type: application/json" --data @/tmp/OIDC_PAYLOAD_TEMP https://{{ .Values.global.masterIP }}:8443/idprovider/v1/auth/registration/$AUTH_ICP_CLIENT_ID >/dev/null
      echo
    fi
    if [[ $REG_CHECK_RESULT = *"invalid_client"* ]]; then
    echo "✖ Client does not exist..."
      #Create registration
      echo "Registering client..."
      curl -k -X POST -u "Authorization: Bearer ${ACCESS_TOKEN}" -H "Content-Type: application/json" --data @/tmp/OIDC_PAYLOAD_TEMP https://{{ .Values.global.masterIP }}:8443/idprovider/v1/auth/registration >/dev/null
      echo
    fi
    #Cleanup - delete the temp payload file with variables replaced
    trap "{ rm -f /tmp/OIDC_PAYLOAD_TEMP; }" EXIT
    echo
    echo Done.

Following is a sample users.yaml file:

{{/*********************************************************** {COPYRIGHT-TOP} ****
* Licensed Materials - Property of IBM
*
* "Restricted Materials of IBM"
*
*  5737-H89, 5737-H64
*
* © Copyright IBM Corp. 2015, 2018  All Rights Reserved.
*
* US Government Users Restricted Rights - Use, duplication, or
* disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
********************************************************* {COPYRIGHT-END} ****/}}
{{- $compName := "cem-users" -}}
{{- include "sch.config.init" (list . "cem.sch.chart.config.values") -}}
{{- $deploymentName := include "sch.names.fullCompName" (list . $compName) -}}
{{- $cdbConfigTemplateName := include "sch.names.volumeClaimTemplateName" (list . "config" $deploymentName) -}}
{{- $rootData := fromYaml (include "root.data" .) -}}
{{- $rootMetering := $rootData.metering -}}
{{- $configMapName := include "sch.names.fullCompName" (list . $compName) -}}
{{- $serviceAccountName := include "sch.names.fullCompName" (list . $compName) -}}
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: {{ $deploymentName }}
  namespace: {{ .Release.Namespace }}
  labels:
{{ include "sch.metadata.labels.standard" (list . $compName) | indent 4 }}
    origin: helm-cem
spec:
  replicas: {{ .Values.cemusers.clusterSize }}
  selector:
    matchLabels:
      release: {{ .Release.Name }}
      app: {{ include "sch.names.appName" (list .) | quote }}
      component: {{ $compName | quote }}
  template:
    metadata:
      labels:
{{ include "sch.metadata.labels.standard" (list . $compName) | indent 8 }}
        origin: helm-cem
      annotations:
        checksum/cemusers-config: {{ include (print $.Template.BasePath "/config/cem-users-config.yaml") . | sha256sum }}
{{- include "sch.metadata.annotations.metering" (list . $rootMetering) | indent 8 }}
    spec:
{{ include "ingress-host-alias" . | indent 6 }}
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          #If you specify multiple nodeSelectorTerms associated with nodeAffinity types,
          #then the pod can be scheduled onto a node if one of the nodeSelectorTerms is satisfied.
          #
          #If you specify multiple matchExpressions associated with nodeSelectorTerms,
          #then the pod can be scheduled onto a node only if all matchExpressions can be satisfied.
          #
          #valid operators: In, NotIn, Exists, DoesNotExist, Gt, Lt
            nodeSelectorTerms:
            - matchExpressions:
              - key: beta.kubernetes.io/arch
                operator: In
                values:
                {{- if .Values.arch }}
                  - {{ .Values.arch }}
                {{- else }}
                  - {{ template "arch" . }}
                {{- end }}
      initContainers:
      - name: create-random-secrets
        image: "{{ .Values.global.image.repository }}/hdm-cem-users:{{ .Values.commonimages.cemusers.image.tag }}"
        command: ["node"]
        args: ["create-secrets.js"]
        env:
        - name: RELEASE
          value: '{{ template "releasename" . }}'
      - name: waitforcouchdb
        image: "{{ .Values.global.image.repository }}/hdm-cem-users:{{ .Values.commonimages.cemusers.image.tag }}"
        command: ["sh", "-c", "i=1;until getent hosts {{ template "releasename" . }}-couchdb.{{ .Release.Namespace }}.svc; do echo waiting for couchdb $i;i=$((i+1)); sleep 2; done;"]
      - name: waitforredis
        image: "{{ .Values.global.image.repository }}/hdm-cem-users:{{ .Values.commonimages.cemusers.image.tag }}"
        command: ["sh", "-c", "i=1;until getent hosts {{ template "releasename" . }}-redis-master-svc.{{ .Release.Namespace }}.svc; do echo waiting for redis $i;i=$((i+1)); sleep 2; done;"]
      containers:
      - name: cem-users
        image: "{{ .Values.global.image.repository }}/hdm-cem-users:{{ .Values.commonimages.cemusers.image.tag }}"
        ports:
        - containerPort: 6002
          protocol: TCP
        livenessProbe:
          tcpSocket:
            port: 6002
          initialDelaySeconds: 120
          periodSeconds: 30
          timeoutSeconds: 20
        readinessProbe:
          tcpSocket:
            port: 6002
          initialDelaySeconds: 20
          timeoutSeconds: 20
        env:
        - name: LICENSE
          value: {{ .Values.license | default "not accepted" }}
        - name: ENV_ICP
          value: "1"
        - name: PORT
          value: "6002"
        - name: BASEURL
          value: '{{ include "cem.services.cemusers" . }}'
{{ include "cloudeventmanagement.cemusers.env" . | indent 8 }}
        - name: VCAP_APPLICATION
          value: '{}'
        - name: INGRESS_PREFIX
          value: '{{ .Values.global.ingress.prefix }}'
        - name: INGRESS_DOMAIN
          value: '{{ .Values.global.ingress.domain }}'
        resources:
{{ include "ibmcemprod.comp.size.data" (list . "cemusers" "resources") | indent 10 }}
        terminationMessagePath: "/dev/termination-log"
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - name: {{ $cdbConfigTemplateName }}
          mountPath: /etc/oidc
      restartPolicy: Always
      terminationGracePeriodSeconds: 30
      dnsPolicy: ClusterFirst
      securityContext:
        runAsUser: 1000
      serviceAccountName: {{ $serviceAccountName }}
      volumes:
        - name: {{ $cdbConfigTemplateName }}
          configMap:
            name: {{ $configMapName }}
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1

After the Helm chart is successfully deployed, a cluster administrator can run the following kubectl command to execute the registration logic and obtain the secret:

kubectl exec -n {{ .Release.Namespace }} -t `kubectl get pods -l release={{ .Release.Name }} -n {{ .Release.Namespace }} | grep "{{ .Release.Name }}-ibm-cem-cem-users" | grep "Running" | head -n 1 | awk '{print $1}'` bash -- "/etc/oidc/oidc_reg.sh" "`echo $(kubectl get secret platform-oidc-credentials -o "jsonpath={.data.OAUTH2_CLIENT_REGISTRATION_SECRET}")`"