Skip to content

Commit

Permalink
Keycloak implementation of SPI (#5)
Browse files Browse the repository at this point in the history
* Create a new Keycloak implementation as a copy of Cognito

* Strip out Cognito specifics

* Create a new client using DCR

* Reimplement create without DCR; implement delete; document auth

* Additional test for deletes

* Get tokens from a helper function

* Enable authorization services on new clients

* Get tokens with middleware

* Use management client as resource server and discover endpoints

* Authorise API products via resources and permissions

* Document Keycloak setup and policy enforcement

* Tests for permissions management

* Remove reference to Keycloak resource server

* fix dev release

* fix dev release - add fallback for release tag

* add keycloak variables in values template

* add id to keycloak oauth credential creation, cognito TODO

* update openapi definition

* add id to e2e tests

* fix unit tests

* remove name from client generation

* fix tests using new id field

* Return created client's name for id

* add GetAPIProducts endpoint + Keycloak implementation

* add GetAPIProducts handler tests

* fix github linter problem

* github review comment fixes

* update Keycloak's UpdateAppAPIProducts method to CRUD as-needed

* update README wording

* update example scope

---------

Co-authored-by: Fabian Gonzalez <[email protected]>
  • Loading branch information
djmcaleese and inFocus7 authored Sep 10, 2024
1 parent 80aaae6 commit c227bca
Show file tree
Hide file tree
Showing 28 changed files with 1,877 additions and 320 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/ci-release-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@ jobs:
- uses: actions/checkout@v4
- id: set_version
run: |
# In order to publish Helm charts we need valid semantic version, so we get the latest release tag to prefix the version with.
git fetch --tags
# Try to get the latest tag, fallback to 0.0.0 if no tag is found
LATEST_RELEASE=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
BRANCH=$(echo $(git rev-parse --abbrev-ref HEAD) | tr -d '0123456789/.')
VERSION=dev-$BRANCH-$(git rev-parse --short HEAD)
VERSION=$LATEST_RELEASE-dev-$BRANCH-$(git rev-parse --short HEAD)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Set version to $VERSION"
docker-release:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,6 @@ _helm_sync_dir/

helm/Chart.yaml
helm/values.yaml

# Built binaries
cmd/idp-connect
103 changes: 99 additions & 4 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,112 @@
## Development
# Development

This is a development guide for users wanting to help contribute to the project.

## Environment

Before you begin, make sure you have all of the required tools installed:

```
```sh
./env/validate-env.sh
```

## TODO:
Add any new connector implementations to `cmd/idp-connect.go` so that they can become valid server options to start.

## Keycloak

You can test the manipulation of self-service clients using a dedicated realm in a Keycloak instance. Create a new realm using curl and the admin credentials using the examples below.

First, create a token to access the Keycloak REST API. This is a short-lived token, so you may need to repeat this step later on:

```sh
KEYCLOAK_URL=http://$(kubectl --context mgmt -n keycloak get service keycloak -o jsonpath='{.status.loadBalancer.ingress[0].*}'):8080

KEYCLOAK_TOKEN=$(curl -Ssm 10 --fail-with-body \
-d "client_id=admin-cli" \
-d "username=admin" \
-d "password=admin" \
-d "grant_type=password" \
"$KEYCLOAK_URL/realms/master/protocol/openid-connect/token" |
jq -r .access_token)
```

Create the new realm:

```sh
REALM=my-realm

curl -Ssm 10 --fail-with-body -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" -H "Content-Type: application/json" \
-d '{ "realm": "'${REALM}'", "enabled": true }' \
$KEYCLOAK_URL/admin/realms
```

You'll need to provision a client in this realm that permits service accounts and has permissions to manipulate self-service clients. For convenience, we'll also treat this client as a _resource server_ in which we will store API products as _resources_. You can create such a client like this:

```sh
KEYCLOAK_CLIENT=gloo-portal

# Create initial token to register the client
INITIAL_TOKEN=$(curl -Ssm 10 --fail-with-body -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" -H "Content-Type: application/json" \
-d '{ "expiration": 0, "count": 1 }' \
$KEYCLOAK_URL/admin/realms/${REALM}/clients-initial-access |
jq -r .token)

# Register the client
read -r KEYCLOAK_CLIENT_INTERNAL_ID KEYCLOAK_SECRET <<<$(curl -Ssm 10 --fail-with-body -H "Authorization: bearer ${INITIAL_TOKEN}" -H "Content-Type: application/json" \
-d '{ "clientId": "'${KEYCLOAK_CLIENT}'", "name": "Solo.io Gloo Portal Resource Server" }' \
${KEYCLOAK_URL}/realms/${REALM}/clients-registrations/default |
jq -r '[.id, .secret] | @tsv')

echo "Management client ID: ${KEYCLOAK_CLIENT}"
echo "Management client secret: ${KEYCLOAK_SECRET}"

# Set up the client as we need
curl -Ssm 10 --fail-with-body -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" -H "Content-Type: application/json" \
-X PUT -d '{ "serviceAccountsEnabled": true, "authorizationServicesEnabled": true }' \
${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${KEYCLOAK_CLIENT_INTERNAL_ID}

# Get the internal ID of the client's service account user
SA_USER_ID=$(curl -Ssm 10 --fail-with-body -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" -H "Content-Type: application/json" \
${KEYCLOAK_URL}/admin/realms/${REALM}/clients/${KEYCLOAK_CLIENT_INTERNAL_ID}/service-account-user |
jq -r .id)

# Get the ID of the 'realm-management' client
REALM_MGMT_CLIENT_ID=$(curl -Ssm 10 --fail-with-body -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" \
"${KEYCLOAK_URL}/admin/realms/${REALM}/clients?clientId=realm-management" |
jq -r '.[].id')

# Get the ID of the 'manage-clients' role
ROLE_ID=$(curl -Ssm 10 --fail-with-body -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" -H "Content-Type: application/json" \
${KEYCLOAK_URL}/admin/realms/${REALM}/users/${SA_USER_ID}/role-mappings/clients/${REALM_MGMT_CLIENT_ID}/available | jq -r '.[] | select(.name=="manage-clients") | .id')

# Add the 'manage-clients' role to the service account user
curl -Ssm 10 --fail-with-body -H "Authorization: Bearer ${KEYCLOAK_TOKEN}" -H "Content-Type: application/json" \
-d '[ { "id": "'${ROLE_ID}'", "name": "manage-clients", "composite": false, "clientRole": true, "containerId": "'${REALM_MGMT_CLIENT_ID}'" } ]' \
${KEYCLOAK_URL}/admin/realms/${REALM}/users/${SA_USER_ID}/role-mappings/clients/${REALM_MGMT_CLIENT_ID}
```

The values of `KEYCLOAK_CLIENT` and `KEYCLOAK_SECRET` should be supplied to the Keycloak flavour of `idp-connect` at runtime (via `--client-id` and `--client-secret`) so that the service can obtain tokens and manipulate self-service clients on behalf of this management client. In the example used so far, you can start the service like this:

```sh
./idp-connect keycloak --issuer ${KEYCLOAK_URL}/realms/${REALM} --client-id ${KEYCLOAK_CLIENT} --client-secret ${KEYCLOAK_SECRET}
```

IDP Connect will use the token endpoint to obtain a token for the management client. You can replicate this for testing purposes like this:

```sh
MGMT_TOKEN=$(curl -Ssm 10 --fail-with-body \
-u ${KEYCLOAK_CLIENT}:${KEYCLOAK_SECRET} \
-d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
-d "audience=${KEYCLOAK_CLIENT}" \
${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/token |
jq -r .access_token)

# Test the token by listing the clients in the realm
curl -Ssm 10 --fail-with-body -H "Authorization: Bearer ${MGMT_TOKEN}" ${KEYCLOAK_URL}/admin/realms/${REALM}/clients | jq .
```

## TODO

* Create middleware to handle login requests and responses and exposing those metrics via Prometheus metrics
* Cognito
* Develop auth mechanism when Cognito is running in EKS, taking advantage of AWS IAM Role for Service Accounts
* Develop auth mechanism when Cognito is running in EKS, taking advantage of AWS IAM Role for Service Accounts
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
# IDP Connect

IDP Connect is an implementation of the Service Programming Interface Gloo Platform Portal uses in order to manage client credentials for accessing services in your Kubernetes Cluster. In Gloo Platform Portal, we use the concept of "Applications" to refer to the external applications accessing the API Products exposed via your Gloo Portal. When a user registers an application as an OAuth client,
it is the responsibility of the SPI to create the credential associated with that application. For more information, and to review key terms associated with Gloo Platform Portal, checkout out our documentation: [Gloo Portal Documentation](https://docs.solo.io/gloo-portal/latest/).
IDP Connect is an implementation of the Service Programming Interface Gloo Gateway Portal uses in order to manage client credentials for accessing services in your Kubernetes Cluster. In Gloo Gateway Portal, we use the concept of "Applications" to refer to the external applications accessing the API Products exposed via your Gloo Portal. When a user registers an application as an OAuth client,
it is the responsibility of the SPI to create the credential associated with that application. For more information, and to review key terms associated with Gloo Gateway Portal, checkout out our documentation: [Gloo Portal Documentation](https://docs.solo.io/gloo-portal/latest/).

## Supported Identity Providers

Here is a list of Identity Providers that we currently support:

* Amazon Cognito
* Keycloak

## Configuration Instructions

### Keycloak

A Keycloak client must be created for the Keycloak IDP Connect service to use. Provide the ID and secret of this client in the `--client-id` and `--client-secret` IDP Connect arguments respectively. This client must meet some requirements:

* The client must have the `manage-client` permission needed for IDP Connect to be able to manipulate self-service clients.
* **Authorization** must be enabled on this client, as this client will also act as an OAuth2 [resource server](https://www.keycloak.org/docs/latest/authorization_services/index.html#_resource_server_overview).
* **Service accounts roles** (or OAuth2 _client credentials_) must be enabled, to allow IDP Connect to use this client directly to manage other clients and resources.

#### Related documentation

* Keycloak's support for client registration: <https://www.keycloak.org/docs/latest/securing_apps/#_client_registration>
* Resource authorization in Keycloak: <https://www.keycloak.org/docs/latest/authorization_services/>
* IDP Connect will manipulate resources using Keycloak's Authorization Services, which is based on [User-Managed Access (UMA)](https://docs.kantarainitiative.org/uma/rec-uma-core.html)

## Production

IDP Connect provides a straightforward and easy-to-setup way of configuring credentials for the applications in your system; however,
we expect that the needs of your system are and will evolve beyond the scope of this simple implementation. The SPI we provide provides a hook on top of which you can build a customizable system to service any number of more advanced use cases.

TODO: Add information for devs

* Install tools
* (Potential) Allow for AWS IAM Roles for service accounts as cognito auth method.
* (Potential) Allow for AWS IAM Roles for service accounts as cognito auth method.
2 changes: 1 addition & 1 deletion api/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Overview

This directory contains the API for Gloo Platform Portal IDP Connect.
This directory contains the API for Gloo Gateway Portal IDP Connect.

## Generation

Expand Down
65 changes: 45 additions & 20 deletions api/v1/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
info:
title: 'Gloo Platform Portal IDP Connect API'
title: 'Gloo Gateway Portal IDP Connect API'
version: 1.0.0
description: Before you begin, set up Gloo Platform Portal with your OpenID Connect (OIDC) provider. Then with this IDP Connect API, you can manage the clients and API Products in your OIDC provider that get associated with your Portal applications. For more information, see the [Gloo Platform Portal docs](https://docs.solo.io/gloo-portal/latest/).
description: Before you begin, set up Gloo Gateway Portal with your OpenID Connect (OIDC) provider. Then with this IDP Connect API, you can manage the clients and API Products in your OIDC provider that get associated with your Portal applications. For more information, see the [Gloo Gateway Portal docs](https://docs.solo.io/gloo-portal/latest/).
openapi: 3.0.0
servers:
- url: https://api.gloo-platform-portal.com/v1
Expand All @@ -11,34 +11,24 @@ paths:
description: Creates an application of type oauth2. This is intended to be integrated with an Open Id Connect Provider that the IDP Connect implementation integrates with. Note that the `clientSecret` is never stored in the database and is shown to the user only once. Keep this secret to make future requests to the API products in the Portal.
operationId: CreateOAuthApplication
requestBody:
description: (Required) name for creating name of the client.
description: (Required) Unique identifier for creating client.
required: true
content:
application/json:
schema:
type: object
required:
- name
- id
properties:
name:
id:
type: string
example: "example-user-pool-developer-1"
example: "a0897e6d0ea94f589c38278bca4e9342"
responses:
'201':
content:
application/json:
schema:
type: object
properties:
clientId:
type: string
example: a0897e6d0ea94f589c38278bca4e9342
clientSecret:
type: string
example: c94dbd582d594e8aa04934f9c7ef0f52
clientName:
type: string
example: "example-user-pool-developer-1"
$ref: '#/components/schemas/OAuthApplication'
description: Successfully created client.
'400':
description: Invalid input.
Expand Down Expand Up @@ -74,7 +64,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
$ref: '#/components/schemas/Error'
'500':
description: Unexpected error deleting application.
content:
Expand Down Expand Up @@ -108,8 +98,8 @@ paths:
apiProducts:
type: array
items:
type: string
example: "example-api-product"
type: string
example: "example-api-product"
responses:
'204':
description: Successfully added API Product to application.
Expand Down Expand Up @@ -174,6 +164,27 @@ paths:
summary: Creates API Product in the OpenID Connect Provider. Then, you can add this API Product to the application for your Portal applications with the `PUT /applications/{id}/api-products` API request.
tags:
- API Products
get:
description: Get all API Products in the Open Id Connect Provider. The Portal uses the results to keep the API Products in the IdP in sync with Portal by creating and deleting as needed.
operationId: GetAPIProducts
responses:
'200':
description: Successfully retrieved API Products.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ApiProduct'
'500':
description: Unexpected error retrieving API Products.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
summary: Get all API Products in the OpenID Connect Provider.
tags:
- API Products
/api-products/{name}:
delete:
description: Deletes API Product in the Open Id Connect Provider for a given unique identifier.
Expand Down Expand Up @@ -215,6 +226,20 @@ components:
description:
type: string
example: "example API Product description"
OAuthApplication:
required:
- clientId
- clientSecret
properties:
clientId:
type: string
example: a0897e6d0ea94f589c38278bca4e9342
clientSecret:
type: string
example: c94dbd582d594e8aa04934f9c7ef0f52
clientName:
type: string
example: "example-user-pool-developer-1"
Error:
required:
- code
Expand Down
2 changes: 2 additions & 0 deletions cmd/idp-connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/spf13/cobra"

"github.com/solo-io/gloo-portal-idp-connect/internal/cognito"
"github.com/solo-io/gloo-portal-idp-connect/internal/keycloak"
"github.com/solo-io/gloo-portal-idp-connect/internal/version"
)

Expand All @@ -28,6 +29,7 @@ func rootCommand(ctx context.Context) *cobra.Command {

cmd.AddCommand(
cognito.Command(),
keycloak.Command(),
)

return cmd
Expand Down
Loading

0 comments on commit c227bca

Please sign in to comment.