Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keycloak implementation of SPI #5

Merged
merged 31 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
266da40
Create a new Keycloak implementation as a copy of Cognito
djmcaleese Mar 27, 2024
a1fb80a
Strip out Cognito specifics
djmcaleese Mar 28, 2024
6b74935
Create a new client using DCR
djmcaleese Mar 28, 2024
d327f8b
Reimplement create without DCR; implement delete; document auth
djmcaleese Apr 2, 2024
0de69c0
Additional test for deletes
djmcaleese Apr 2, 2024
ab84f52
Get tokens from a helper function
djmcaleese Apr 2, 2024
e3bdd56
Enable authorization services on new clients
djmcaleese Apr 2, 2024
ea26623
Get tokens with middleware
djmcaleese Apr 16, 2024
3880dd1
Use management client as resource server and discover endpoints
djmcaleese Apr 16, 2024
7a3051b
Authorise API products via resources and permissions
djmcaleese Apr 17, 2024
5083150
Document Keycloak setup and policy enforcement
djmcaleese Apr 19, 2024
622087a
Tests for permissions management
djmcaleese Apr 19, 2024
f91cc03
Remove reference to Keycloak resource server
djmcaleese Apr 19, 2024
c4f0ce6
Merge branch 'main' into declan/keycloak
inFocus7 Aug 26, 2024
63e5aee
fix dev release
inFocus7 Aug 26, 2024
945d199
fix dev release - add fallback for release tag
inFocus7 Aug 26, 2024
6bad863
add keycloak variables in values template
inFocus7 Aug 27, 2024
facafe7
add id to keycloak oauth credential creation, cognito TODO
inFocus7 Aug 27, 2024
10abb1e
update openapi definition
inFocus7 Aug 27, 2024
c8f70b6
add id to e2e tests
inFocus7 Aug 27, 2024
7a777d8
fix unit tests
inFocus7 Aug 27, 2024
1827bcb
remove name from client generation
inFocus7 Aug 27, 2024
6f7eec0
fix tests using new id field
inFocus7 Aug 27, 2024
008713b
Return created client's name for id
inFocus7 Aug 28, 2024
868bd4c
add GetAPIProducts endpoint + Keycloak implementation
inFocus7 Sep 4, 2024
219f2b7
add GetAPIProducts handler tests
inFocus7 Sep 5, 2024
196c208
fix github linter problem
inFocus7 Sep 5, 2024
27a3d7e
github review comment fixes
inFocus7 Sep 6, 2024
2827776
update Keycloak's UpdateAppAPIProducts method to CRUD as-needed
inFocus7 Sep 9, 2024
0ed2b52
update README wording
inFocus7 Sep 9, 2024
e2c58b1
update example scope
inFocus7 Sep 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,30 @@ it is the responsibility of the SPI to create the credential associated with tha
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.
61 changes: 43 additions & 18 deletions api/v1/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading