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

feat: create module for configuring an okta app with a kms key #691

Merged
merged 7 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions kms-okta-app/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Auto-generated by fogg. Do not edit
# Make improvements in fogg, so that everyone can benefit.

export TERRAFORM_VERSION := 1.3.6
export TF_PLUGIN_CACHE_DIR := ../../..//.terraform.d/plugin-cache

include ../../..//scripts/module.mk


help: ## display help for this makefile
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.PHONY: help
85 changes: 85 additions & 0 deletions kms-okta-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
This is a module that creates an Okta app that's bounded to a KMS Key. You can use the KMS key to sign JWTs that are compatible with the Okta /token endpoints. After you create the Okta App, you can work with the Okta Token endpoint by roughly following these steps:
1. Creating a header with this kind of structure: `{{"alg": "<algorithm>", "typ": "JWT"}}`. To identify algorithms. Here are the algorithm options as of Febuary 5, 2025:
```
signing_algs = (
('RSASSA_PSS_SHA_256', 'PS256', 'sha256'),
('RSASSA_PSS_SHA_384', 'PS384', 'sha384'),
('RSASSA_PSS_SHA_512', 'PS512', 'sha512'),
('RSASSA_PKCS1_V1_5_SHA_256', 'RS256', 'sha256'),
('RSASSA_PKCS1_V1_5_SHA_384', 'RS384', 'sha384'),
('RSASSA_PKCS1_V1_5_SHA_512', 'RS512', 'sha512'),
('ECDSA_SHA_256', 'ES256', 'sha256'),
('ECDSA_SHA_384', 'ES384', 'sha384'),
('ECDSA_SHA_512', 'ES512', 'sha512'),
)
```
source: https://github.com/jmtapio/python-jwt-kms/blob/552668588c5eec5eff9346740660239baef22428/jwt_kms/jwa.py
So if your KMS key has type `RSASSA_PKCS1_V1_5_SHA_256`, your `alg` is `RS256`. If you want to dive deeper, you can look at the RFC section here: https://datatracker.ietf.org/doc/html/rfc7518#section-3.1

2. Creating a payload with this kind of structure with this format:
```json
{
"exp": <future timestamp>,
"iat": <number of seconds since Jan 1, 1970 UTC>,
"iss": client_id,
"aud": [okta_token_endpoint],
"sub": client_id,
}
```
each attribute can be found here: https://developer.okta.com/docs/api/openapi/okta-oauth/guides/client-auth/#token-claims-for-client-authentication-with-client-secret-or-private-key-jwt

3. Base64-encode values from step #1 and #2 and strip out the equal signs (`=`).
4. Concatenate the values from step #3 with a header.payload format, then use the AWS KMS `sign` operation from whatever AWS SDK you use. If you used algorithm type `RS256`, then your KMS Sign operation should use SigningAlgorithm type `RSASSA_PKCS1_V1_5_SHA_256`. You should be able to parse out the "Signature" value from your kms `sign` output.
5. Construct the JWT in this way: base64header.base64payload.signature-from-step-4

After you construct the JWT, you're ready to use it with the Okta Token endpoint here: https://developer.okta.com/docs/api/openapi/okta-oauth/guides/client-auth/#jwt-with-private-key
you just need to set the JWT from step 5 to the "client_assertion" value while making the request.

<!-- START -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3 |
| <a name="requirement_okta"></a> [okta](#requirement\_okta) | ~> 4.0 |

## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | n/a |
| <a name="provider_external"></a> [external](#provider\_external) | n/a |
| <a name="provider_okta"></a> [okta](#provider\_okta) | ~> 4.0 |

## Modules

| Name | Source | Version |
|------|--------|---------|
| <a name="module_params"></a> [params](#module\_params) | github.com/chanzuckerberg/cztack//aws-ssm-params-writer | v0.63.3 |

## Resources

| Name | Type |
|------|------|
| [okta_app_oauth.idp_api](https://registry.terraform.io/providers/okta/okta/latest/docs/resources/app_oauth) | resource |
| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source |
| [external_external.jwks_info](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_friendly_key_identifier"></a> [friendly\_key\_identifier](#input\_friendly\_key\_identifier) | A name for the key configuration in the okta app, something you will recognize for yourself and the project. | `string` | n/a | yes |
| <a name="input_kms_key_id"></a> [kms\_key\_id](#input\_kms\_key\_id) | The Key ID or alias of the AWS KMS Key. It has to be available in the same region and account as the configured provider. | `string` | n/a | yes |
| <a name="input_okta_configuration"></a> [okta\_configuration](#input\_okta\_configuration) | Details needed to configure an okta app. Its token auth method is private\_key\_jwt | <pre>object({<br> label = string<br> type = string<br> grant_types = list(string)<br> omit_secret = bool<br> response_types = list(string)<br> pkce_required = bool<br> })</pre> | n/a | yes |
| <a name="input_tags"></a> [tags](#input\_tags) | These values are used to derive the path in the param store where to write the Okta App Configuration metadata. | <pre>object({<br> project = string,<br> env = string,<br> service = string,<br> owner = string,<br> })</pre> | n/a | yes |
| <a name="input_write_metadata_to_params"></a> [write\_metadata\_to\_params](#input\_write\_metadata\_to\_params) | Whether you want to include the clientID and KMS Key Alias grouped together as securestring parameters in JSON format. If true, module will write these details to a path based on the env, project and service"<br> They will be written following path:<br> /<project>-<env>-<service>/client\_id<br> /<project>-<env>-<service>/kms\_key\_id<br><br> (the module may add secrets over time)<br><br> Note that these values should correspond with the consuming service's tagset so secrets are placed in the path they expect. | `bool` | n/a | yes |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_client_id"></a> [client\_id](#output\_client\_id) | n/a |
| <a name="output_kms_id"></a> [kms\_id](#output\_kms\_id) | n/a |
<!-- END -->
2 changes: 2 additions & 0 deletions kms-okta-app/fogg.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Auto-generated by fogg. Do not edit
# Make improvements in fogg, so that everyone can benefit.
28 changes: 28 additions & 0 deletions kms-okta-app/get_jwks_for_okta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import boto3, json
from cryptography.hazmat.primitives.serialization import load_der_public_key
from jose import jwk, constants
import sys

aws_account_id = sys.argv[1]
kms_key_id = sys.argv[2]
region = sys.argv[3]

sts_client = boto3.client("sts", region_name=region)
assume_role_client = sts_client.assume_role(RoleArn=f"arn:aws:iam::{aws_account_id}:role/tfe-si", RoleSessionName='FetchKMSInformation')
credentials = assume_role_client["Credentials"]
kms_session = boto3.Session(
aws_access_key_id=credentials["AccessKeyId"],
aws_secret_access_key= credentials["SecretAccessKey"],
aws_session_token= credentials["SessionToken"],
region_name=region,
)
kms_client = kms_session.client("kms", region_name=region)

# try to get the public key in bytes
output = kms_client.get_public_key(
KeyId=kms_key_id,
)
assert output.get("PublicKey") is not None
public_key = load_der_public_key(output["PublicKey"])
jwks_vals = jwk.RSAKey(algorithm=constants.Algorithms.RS256, key=public_key).to_dict()
print(json.dumps(jwks_vals))
35 changes: 35 additions & 0 deletions kms-okta-app/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
# uses a key in core-platform-infra to generate credentials for this workspace
data "external" "jwks_info" {
program = ["bash", "${path.module}/run.sh", path.module, data.aws_caller_identity.current.account_id, var.kms_key_id, data.aws_region.current.name]
}

resource "okta_app_oauth" "idp_api" {
label = var.okta_configuration.label
type = var.okta_configuration.type
grant_types = var.okta_configuration.grant_types
response_types = var.okta_configuration.response_types
token_endpoint_auth_method = "private_key_jwt"
pkce_required = var.okta_configuration.pkce_required
jwks {
e = data.external.jwks_info.result.e
kty = data.external.jwks_info.result.kty
kid = var.friendly_key_identifier
n = data.external.jwks_info.result.n
}
}


module "params" {
source = "github.com/chanzuckerberg/cztack//aws-ssm-params-writer?ref=v0.63.3"
project = var.tags.project
env = var.tags.env
service = var.tags.service
owner = var.tags.owner

parameters = {
"client_id" = okta_app_oauth.idp_api.client_id
"kms_key_id" = var.kms_key_id
}
}
7 changes: 7 additions & 0 deletions kms-okta-app/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
output "client_id" {
value = okta_app_oauth.idp_api.id
}

output "kms_id" {
value = var.kms_key_id
}
5 changes: 5 additions & 0 deletions kms-okta-app/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh

# need to install python-jose instead of jose: https://stackoverflow.com/a/65103147
pip install boto3 cryptography python-jose 2>&1 > /dev/null
python3 $1/get_jwks_for_okta.py $2 $3 $4
4 changes: 4 additions & 0 deletions kms-okta-app/terragrunt.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dependencies {
paths = [
]
}
48 changes: 48 additions & 0 deletions kms-okta-app/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
variable "kms_key_id" {
type = string
description = "The Key ID or alias of the AWS KMS Key. It has to be available in the same region and account as the configured provider."
}

variable "okta_configuration" {
type = object({
label = string
type = string
grant_types = list(string)
omit_secret = bool
response_types = list(string)
pkce_required = bool
})
description = "Details needed to configure an okta app. Its token auth method is private_key_jwt"
}

variable "friendly_key_identifier" {
type = string
description = "A name for the key configuration in the okta app, something you will recognize for yourself and the project."
}

variable "write_metadata_to_params" {
type = bool
description = <<EOF
Whether you want to include the clientID and KMS Key Alias grouped together as securestring parameters in JSON format. If true, module will write these details to a path based on the env, project and service"
They will be written following path:
/<project>-<env>-<service>/client_id
/<project>-<env>-<service>/kms_key_id

(the module may add secrets over time)

Note that these values should correspond with the consuming service's tagset so secrets are placed in the path they expect.
EOF
}

variable "tags" {
type = object({
project = string,
env = string,
service = string,
owner = string,
})

description = <<EOF
These values are used to derive the path in the param store where to write the Okta App Configuration metadata.
EOF
}
10 changes: 10 additions & 0 deletions kms-okta-app/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
terraform {
required_providers {
okta = {
source = "okta/okta"
version = "~> 4.0"
}
}

required_version = ">= 1.3"
}
Loading