Skip to content

Commit

Permalink
feat: add service accounts options to sso (#852)
Browse files Browse the repository at this point in the history
## Description
This enables support for service account roles in keycloak for client
credentials type grants
...

## Related Issue

Fixes #851

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Other (security config, docs update, etc)

## Checklist before merging

- [x] Test, docs, adr added or updated as needed
- [x] [Contributor
Guide](https://github.com/defenseunicorns/uds-template-capability/blob/main/CONTRIBUTING.md)
followed

---------

Co-authored-by: Blake Burkhart <blake@defenseunicorns.com>
Co-authored-by: Micah Nagel <micah.nagel@defenseunicorns.com>
  • Loading branch information
3 people authored and docandrew committed Oct 17, 2024
1 parent c9db656 commit 1f4df34
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 2 deletions.
38 changes: 38 additions & 0 deletions docs/configuration/uds-operator.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,44 @@ spec:

This configuration does not create a secret in the cluster and instead tells the UDS Operator to create a public client (one that requires no auth secret) that enables the `oauth2.device.authorization.grant.enabled` flow and disables the standard redirect auth flow. Because this creates a public client configuration that deviates from this is limited - if your application requires both the Device Authorization Grant and the standard flow this is currently not supported without creating two separate clients.

### Creating a UDS Package with a Service Account Roles client

Some applications may need to access resources / obtain OAuth tokens on behalf of *themselves* vice users. This may be needed to allow API access to Authservice protected applications (outside of a web browser). This is commonly used in machine-to-machine authentication for automated processes. This type of grant in OAuth 2.0 is known as the [Client Credentials Grant](https://oauth.net/2/grant-types/client-credentials/) and is supported in a UDS Package with the following configuration:

```yaml
apiVersion: uds.dev/v1alpha1
kind: Package
metadata:
name: client-cred
namespace: argo
spec:
sso:
- name: httpbin-api-client
clientId: httpbin-api-client
standardFlowEnabled: false
serviceAccountsEnabled: true
# By default, Keycloak will not set the audience `aud` claim for service account access token JWTs.
# You can optionally add a protocolMapper to set the audience.
# If you map the audience to the same client used for authservice, you can enable access to authservice protected apps with a service account JWT.
protocolMappers:
- name: audience
protocol: "openid-connect"
protocolMapper: "oidc-audience-mapper"
config:
included.client.audience: "uds-core-httpbin" # Set this to match the app's authservice client id
access.token.claim: "true"
introspection.token.claim: "true"
id.token.claim: "false"
lightweight.claim: "false"
userinfo.token.claim: "false"
```
Setting `serviceAccountsEnabled: true` requires `standardFlowEnabled: false` and is incompatible with `publicClient: true`.

If needed, multiple clients can be added to the same application: an AuthService client, a device flow client, and as many service account clients as required.

A keycloak service account JWT can be distinguished by a username prefix of `service-account-` and a new claim called `client_id`. Note that the `aud` field is not set by default, hence the mapper in the example.

### SSO Client Attribute Validation

The SSO spec supports a subset of the Keycloak attributes for clients, but does not support all of them. The current supported attributes are:
Expand Down
4 changes: 4 additions & 0 deletions src/pepr/operator/crd/generated/package-v1alpha1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,10 @@ export interface Sso {
* Enables the standard OpenID Connect redirect based authentication with authorization code.
*/
standardFlowEnabled?: boolean;
/**
* Enables the client credentials grant based authentication via OpenID Connect protocol.
*/
serviceAccountsEnabled?: boolean;
/**
* Allowed CORS origins. To permit all origins of Valid Redirect URIs, add '+'. This does
* not include the '*' wildcard though. To permit all origins, explicitly add '*'.
Expand Down
6 changes: 6 additions & 0 deletions src/pepr/operator/crd/sources/package/v1alpha1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,12 @@ const sso = {
type: "boolean",
default: true,
},
serviceAccountsEnabled: {
description:
"Enables the client credentials grant based authentication via OpenID Connect protocol.",
type: "boolean",
default: false,
},
publicClient: {
description: "Defines whether the client requires a client secret for authentication",
type: "boolean",
Expand Down
48 changes: 48 additions & 0 deletions src/pepr/operator/crd/validators/package-validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,38 @@ describe("Test validation of Exemption CRs", () => {
expect(mockReq.Deny).toHaveBeenCalledTimes(1);
});

it("denies public clients using the service accounts roles", async () => {
const mockReq = makeMockReq(
{},
[],
[],
[
{
publicClient: true,
serviceAccountsEnabled: true,
},
],
);
await validator(mockReq);
expect(mockReq.Deny).toHaveBeenCalledTimes(1);
});

it("denies using standard flow with service accounts roles", async () => {
const mockReq = makeMockReq(
{},
[],
[],
[
{
standardFlowEnabled: true,
serviceAccountsEnabled: true,
},
],
);
await validator(mockReq);
expect(mockReq.Deny).toHaveBeenCalledTimes(1);
});

it("denies public device flow clients using a secret", async () => {
const mockReq = makeMockReq(
{},
Expand Down Expand Up @@ -397,6 +429,22 @@ describe("Test validation of Exemption CRs", () => {
expect(mockReq.Approve).toHaveBeenCalledTimes(1);
});

it("allows service account clients with standard flow disabled ", async () => {
const mockReq = makeMockReq(
{},
[],
[],
[
{
serviceAccountsEnabled: true,
standardFlowEnabled: false,
},
],
);
await validator(mockReq);
expect(mockReq.Approve).toHaveBeenCalledTimes(1);
});

it("denies authservice clients with : in client ID", async () => {
const mockReq = makeMockReq(
{},
Expand Down
11 changes: 9 additions & 2 deletions src/pepr/operator/crd/validators/package-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,17 @@ export async function validator(req: PeprValidateRequest<UDSPackage>) {
`The client ID "${client.clientId}" must specify redirectUris if standardFlowEnabled is turned on (it is enabled by default)`,
);
}
// If serviceAccountsEnabled is true, do not allow standard flow
if (client.serviceAccountsEnabled && client.standardFlowEnabled) {
return req.Deny(
`The client ID "${client.clientId}" serviceAccountsEnabled is disallowed with standardFlowEnabled`,
);
}
// If this is a public client ensure that it only sets itself up as an OAuth Device Flow client
if (
client.publicClient &&
(client.standardFlowEnabled !== false ||
(client.standardFlowEnabled !== false /* default true */ ||
client.serviceAccountsEnabled /* default false */ ||
client.secret !== undefined ||
client.secretName !== undefined ||
client.secretTemplate !== undefined ||
Expand All @@ -152,7 +159,7 @@ export async function validator(req: PeprValidateRequest<UDSPackage>) {
client.attributes?.["oauth2.device.authorization.grant.enabled"] !== "true")
) {
return req.Deny(
`The client ID "${client.clientId}" must _only_ configure the OAuth Device Flow as a public client`,
`The client ID "${client.clientId}" sets options incompatible with publicClient`,
);
}
// Check if client.attributes contain any disallowed attributes
Expand Down

0 comments on commit 1f4df34

Please sign in to comment.