Handling Crypto Material

Handling Crypto Material

Declaring the keys on services

OpenID Connect servers are required to issue ID Tokens and very often OAuth servers are required to issue access tokens in JWT format. Those formats are required to be signed and optionally encrypted, and as a requirement for signing and encrypting, the server is required to possess private keys and make public keys available to clients.

The Authlete Terraform provider allow public and private keys to be defined in hcl scripts using the jwk objects. Those objects must be defined on authlete_service and authlete_client scope and the semantics of the tag is straightforward: if the object is defined on an authlete_server, it represents a key possessed by the server, and if defined on an authlete_client it represents a key possessed by the client.

The jwk takes a format as below, where the first jwk block is an elliptic curve to be used for signing the access token, and the second block is a symmetric key to be used for encryption.

resource "authlete_service" "as" {
  issuer = "https://as.mydomain.com"
  service_name = "MyDomainAS"
  description = "A terraform based service for managing the Authlete based OAuth server"
  supported_grant_types = ["AUTHORIZATION_CODE"]
  supported_response_types = ["CODE"]
  access_token_sign_alg = "ES256"
  access_token_signature_key_id = "ec1"

  jwk {
    kid = "ec1"
    crv = "P-256"
    kty = "EC"
    d = "VT0W-vHxG8Wc0Ev0UT1jIs0XKfctQfQc93WV5Bqb2a0"
    use = "sig"
    x = "coUEzc60fSaVWui-NCUEqAKwFq_isrQbdcxk-jafyTw"
    y = "b9hCE1LgOry4mEUFgfz49NBEiNuC5mbBgb9glVZp420"
    alg = "ES256"
  }
  
  
  jwk {
    kid = "sym1"
    kty = "oct"
    alg = "A128KW"
    k   = "GawgguFyGrWKav7AX4VKUg"
    use = "enc"
  }
  
}

Every key attribute is marked as sensitive, so they will not be echoed to Terraform console. If you provision the above service (source under key_material folder), the output will be as below:

% terraform apply --auto-approve

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # authlete_service.as will be created
  + resource "authlete_service" "as" {
      + access_token_sign_alg                 = "ES256"
      + access_token_signature_key_id         = "ec1"
      + access_token_type                     = (known after apply)
      + api_secret                            = (known after apply)
      + client_id_alias_enabled               = false
      + dcr_scope_used_as_requestable         = (known after apply)
      + description                           = "A terraform based service for managing the Authlete based OAuth server"
      + direct_authorization_endpoint_enabled = false
      + direct_introspection_endpoint_enabled = (known after apply)
      + direct_jwks_endpoint_enabled          = false
      + direct_revocation_endpoint_enabled    = (known after apply)
      + direct_token_endpoint_enabled         = (known after apply)
      + direct_user_info_endpoint_enabled     = false
      + id                                    = (known after apply)
      + ignore_port_loopback_redirect         = (known after apply)
      + issuer                                = "https://as.mydomain.com"
      + service_name                          = "MyDomainAS"
      + single_access_token_per_subject       = (known after apply)
      + supported_claim_types                 = (known after apply)
      + supported_displays                    = (known after apply)
      + supported_grant_types                 = [
          + "AUTHORIZATION_CODE",
        ]
      + supported_introspection_auth_methods  = (known after apply)
      + supported_response_types              = [
          + "CODE",
        ]
      + supported_revocation_auth_methods     = (known after apply)
      + supported_token_auth_methods          = (known after apply)

      + jwk {
          + alg      = "ES256"
          + crv      = "P-256"
          + d        = (sensitive value)
          + generate = false
          + kid      = "ec1"
          + kty      = "EC"
          + use      = "sig"
          + x        = (sensitive value)
          + y        = (sensitive value)
        }
      + jwk {
          + alg      = "A128KW"
          + generate = false
          + k        = (sensitive value)
          + kid      = "sym1"
          + kty      = "oct"
          + use      = "enc"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + api_key    = (known after apply)
  + api_secret = (sensitive value)
authlete_service.as: Creating...
authlete_service.as: Creation complete after 5s [id=8027916614481]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

api_key = "8027916614481"
api_secret = <sensitive>

The Authlete server supports all the algorithms and methods from JWS and JWE plus extensions. You can check all the supported algorithms on the Specification page, and the Terraform provider support all of them.

Random generated keys

Supported Algorithms and Curves for key generation

The rule of thumb for private keys is that the exposure should be minimal or none at all. This means that creating keys on systems with proper randomness and moving it to the server is quite a challenge. In order to reduce the exposure, the Terraform provider can generate keys and configure the Authlete server with that.

For RSA cryptosystem, the provider supports generating keys for all the algorithms: RS256, RS384, RS512, PS256, PS384, PS512, RSA-OAEP, and RSA-OAEP-256. The supported Elliptic-curve algorithms are: ES256, ES384, ES512, ECDH-ES, ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW, Ed25519, and X25519.

The declaration of a key in Terraform scripts is as simple as it can gets: you declare the key, include the attribute generate as true, and omit the key attributes. When provisioning the service in Authlete the provider will identify which keys are to be generated or removed from the server and act accordingly.

Declaration of a key to be generated is done like below:

resource "authlete_service" "as" {
  issuer = "https://as.mydomain.com"
  service_name = "MyDomainAS"
  description = "A terraform based service for managing the Authlete based OAuth server"
  supported_grant_types = ["AUTHORIZATION_CODE"]
  supported_response_types = ["CODE"]
  access_token_sign_alg = "ES256"
  access_token_signature_key_id = "ec2"

  jwk {
    kid = "ec2"
	alg = "ES256" 
	use = "sig"
    crv = "P-256"
    generate = true
  }
}

RSA keys

RSA cryptosystem is quite established, pervasive, and key generation is very well-known, but the Authlete provider can handle that for you, independent of the key size.

The example below, from rsa_key_gen folder of authlete-terraform-samples project, shows the declaration of RSA keys of multiple sizes and usages.

The declaration below can generate different keys on the server.

% cat main.tf
resource "authlete_service" "as" {
  issuer = "https://as.mydomain.com"
  service_name = "MyDomainAS"
  description = "A terraform based service for managing the Authlete based OAuth server"
  supported_grant_types = ["AUTHORIZATION_CODE"]
  supported_response_types = ["CODE"]
  access_token_sign_alg = "RS256"
  access_token_signature_key_id = "rsa1"

  jwk {
    kid = "rsa1"
    alg = "RS256"
    use = "sig"
    kty = "RSA"
    key_size = 2048
    generate = true
  }

  jwk {
    kid = "rsa2"
    alg = "RS384"
    use = "sig"
    kty = "RSA"
    key_size = 2048
    generate = true
  }

  jwk {
    kid = "rsa3"
    alg = "PS512"
    use = "sig"
    kty = "RSA"
    key_size = 2048
    generate = true
  }

  jwk {
    kid = "rsa4"
    alg = "RSA-OAEP"
    use = "enc"
    kty = "RSA"
    key_size = 2048
    generate = true
  }

  jwk {
    kid = "rsa5"
    alg = "RSA-OAEP"
    use = "enc"
    kty = "RSA"
    key_size = 4096
    generate = true
  }

  jwk {
    kid = "rsa6"
    alg = "RSA-OAEP-256"
    use = "enc"
    kty = "RSA"
    key_size = 4096
    generate = true
  }
}
% terraform apply --auto-approve

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # authlete_service.as will be created
  + resource "authlete_service" "as" {
      + access_token_sign_alg                 = "RS256"
      + access_token_signature_key_id         = "rsa1"
      + access_token_type                     = (known after apply)
      + api_secret                            = (known after apply)
      + client_id_alias_enabled               = false
      + dcr_scope_used_as_requestable         = (known after apply)
      + description                           = "A terraform based service for managing the Authlete based OAuth server"
      + direct_authorization_endpoint_enabled = false
      + direct_introspection_endpoint_enabled = (known after apply)
      + direct_jwks_endpoint_enabled          = false
      + direct_revocation_endpoint_enabled    = (known after apply)
      + direct_token_endpoint_enabled         = (known after apply)
      + direct_user_info_endpoint_enabled     = false
      + id                                    = (known after apply)
      + ignore_port_loopback_redirect         = (known after apply)
      + issuer                                = "https://as.mydomain.com"
      + service_name                          = "MyDomainAS"
      + single_access_token_per_subject       = (known after apply)
      + supported_claim_types                 = (known after apply)
      + supported_displays                    = (known after apply)
      + supported_grant_types                 = [
          + "AUTHORIZATION_CODE",
        ]
      + supported_introspection_auth_methods  = (known after apply)
      + supported_response_types              = [
          + "CODE",
        ]
      + supported_revocation_auth_methods     = (known after apply)
      + supported_token_auth_methods          = (known after apply)

      + jwk {
          + alg      = "RS256"
          + generate = true
          + key_size = 2048
          + kid      = "rsa1"
          + kty      = "RSA"
          + use      = "sig"
        }
      + jwk {
          + alg      = "RS384"
          + generate = true
          + key_size = 2048
          + kid      = "rsa2"
          + kty      = "RSA"
          + use      = "sig"
        }
      + jwk {
          + alg      = "PS512"
          + generate = true
          + key_size = 2048
          + kid      = "rsa3"
          + kty      = "RSA"
          + use      = "sig"
        }
      + jwk {
          + alg      = "RSA-OAEP"
          + generate = true
          + key_size = 2048
          + kid      = "rsa4"
          + kty      = "RSA"
          + use      = "enc"
        }
      + jwk {
          + alg      = "RSA-OAEP"
          + generate = true
          + key_size = 4096
          + kid      = "rsa5"
          + kty      = "RSA"
          + use      = "enc"
        }
      + jwk {
          + alg      = "RSA-OAEP-256"
          + generate = true
          + key_size = 4096
          + kid      = "rsa6"
          + kty      = "RSA"
          + use      = "enc"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + api_key    = (known after apply)
  + api_secret = (sensitive value)
authlete_service.as: Creating...
authlete_service.as: Creation complete after 8s [id=8033504653714]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

api_key = "8033504653714"
api_secret = <sensitive>

If we look at the Authlete service definition, the keys will be populated as the screenshot below:

Generated RSA keys

The keys are generated on the Terraform client, pushed to the server, and NOT pushed to state. This means you can’t recover the generated keys from Terraform state. If you need to do so, you need to import the service definition to Terraform.

Another key point is that the x5c attribute is not populated, as the certificate itself is not present.

Elliptic Curve keys

The key generation in Elliptic Curve space is less well understood as the adoption of EC is increasing and the math behind it is more complex with increasing coverage in the computer science syllabus. Independent of the complexity, the Authlete Terraform provider hides that from you, as for the RSA keys: you declare a key to be generated, the algorithm, and the curve to be used, and it will be generated for you.

Some attributes are common to RSA keys, like kid, kty, alg, and use, while crv is a specific attribute for this type of key.

As sample of configuration you can check the ec_key_gen folder on the Authlete Terraform samples project. In the example below, you have all sorts of Elliptic Curve keys declared, including EdDSA algorithm with Curve25519 curve.

% cat main.tf
resource "authlete_service" "as" {
  issuer = "https://as.mydomain.com"
  service_name = "MyDomainAS"
  description = "A terraform based service for managing the Authlete based OAuth server"
  supported_grant_types = ["AUTHORIZATION_CODE"]
  supported_response_types = ["CODE"]
  access_token_sign_alg = "ES256"
  access_token_signature_key_id = "ec1"
  jwk {
    kid = "ec1"
    alg = "ES256"
    use = "sig"
    crv = "P-256"
    generate = true
  }
  jwk {
    kid = "ec3"
    alg = "ES384"
    use = "sig"
    crv = "P-384"
    generate = true
  }
  jwk {
    kid = "ec4"
    alg = "ES512"
    use = "sig"
    crv = "P-521"
    generate = true
  }
  jwk {
    kid = "enc1"
    alg = "ECDH-ES"
    use = "enc"
    crv = "P-256"
    generate = true
  }
  jwk {
    kid = "enc2"
    alg = "ECDH-ES+A128KW"
    use = "enc"
    crv = "P-256"
    generate = true
  }
  jwk {
    kid = "enc3"
    alg = "ECDH-ES+A192KW"
    use = "enc"
    crv = "P-256"
    generate = true
  }
  jwk {
    kid = "ed1"
    alg = "EdDSA"
    use = "sig"
    crv = "Ed25519"
    kty = "OKP"
    generate = true
  }
}

PEM support

If you need to use sign and/or encryption keys associated with signed certificates, you will be required to create the keys, create the certificate, and CSR externally to Authlete.

To reduce the exposure of the private key, the Authlete provider supports key in PEM format, either RSA or EC keys. The provider does the conversion locally and submits in JWK format to the Authlete server.

The attribute used if the pem_private_key and the declarations are as below:

resource "authlete_service" "rsa" {
  issuer                        = "https://test.com"
  service_name                  = "RSA Test API"
  supported_grant_types         = ["AUTHORIZATION_CODE", "REFRESH_TOKEN"]
  supported_response_types      = ["CODE"]
  access_token_sign_alg         = "RS256"
  access_token_signature_key_id = "rsa1"
  jwk {
    kid             = "rsa1"
    alg             = "RS256"
    use             = "sig"
    pem_private_key = "-----BEGIN RSA PRIVATE KEY-----\r\nMIIEpAIBAAKCAQEA71UPBn2cS7qP89sdIlWEv2KrsTopLuWeIpbzB98V8U1OIvb0\r\nYPcCHtpLq+P8u1aceyPotR3AW49BIJ4VzPdTSx+rMmBV0iNv4y289eEZa5Ipvk9T\r\nFtEmf7vR6ZMmM1xK7+fcYyf5AIhcZClt5OrFpTboHYadJ5l/rjpRSNxE7i7b34Bi\r\n1A/HEgmA3GuPV8yf8nDRwGtzBC+nd5tX7gugDbVw/5fF+HDBGcB4u7Fm6fK6T4C3\r\n7ohxvI6RWphB3AuEa+UdkR9ceill1Pz0ID+SLdO2Jt+DnxNCNqBa0ezLY70g0no6\r\nYkvLcnzbaNh82yE28p1IhweF4CP4b6NyPDIisQIDAQABAoIBAQCoSBWdibrZIJ/R\r\nZjLxDlKdw4JXxj5o5DkxtxPBaCHknmeffCdO+r959CIbBd6R1w+GIjShDP9RIcQ4\r\nbA+GJC1j+CuG62fMrvAgO+vOs20NTyOc1efldkBstiKd6sKEgJOMZmp3KgcSUc8s\r\n+lh0CoPYbGf/QsTDsFGvrv+yjHbHRb1bcQjNZCE77Vr9SvdVFOph8750DvftwHdy\r\nvZKe9u3VjcC0LGA1qFZeUgwfynNaGcxwiZ/gZ2vnAbAW+g7YpfdqTs4l32yQAluw\r\n3Ctg1pYtzz4M1iX4OyX5LWMS/P/1Xr9fXLGS0H8EYoACb+mb5LOTR6xWHQZpfo7A\r\nmTErM6lxAoGBAPwyn//oh7XY32BGoGGofkUhoD5p7c+OUKjHpSysH6AP31ZYAA3N\r\nY5hsRCY9hHsF08EWpdfN+i6oPTTYATb8hXJ+5MotrehsitnubjkE5uuym5sXf040\r\nDorD0/oH2WyICenNryyWqx1uRAJlmrZoBe5dJ4hEzkv6pAwMu3HgyTZtAoGBAPLw\r\nx0ZEr32gTbGSpNvxHUfwZY2qtuG9CLQ3MR8JwkkK91RqOKiB2LLjP7LoiWE4BLF9\r\nCsLT4MDDXcWMcGCsQ8bTbapPgdE1uuGAyzpuigQMn/FwNLjHnaJlpvX5EZT9AauS\r\nNkNV/EGojIhXsJ3sfyU6qRoeeOkmobzqDybe5wLVAoGAT8u01EO+rMrx4oR2OnAV\r\ng8of6Z+anxFoc/63RGsxlnNvNuKhIbzaxl97MJ5GTKaLWYzQ7Hc/sYOJ2i5+M+ey\r\nUYfU3COX4vJ0/H90YJYsemcI1QmaPiQ6da2AZJwXLz/b4x4xTupdOfKpkhiT2yMO\r\nvVy8JWGf5GppfWaJ6H43LAECgYBc+xCZ8VHlWAREcWbNkzPsw7JqjSsfrNT2/KS9\r\nR2Pnxt2wnlL/E2tX1CgeFmf2IJWTRNNoi+VagauTH1Qne+cY4vT3GSULaHAVPNEL\r\nlSEXualBo/tZuXS4ogVL4T78cfVAsF46WV+J1bOrvzwmxUxIeHIeQAlw2stOXZrc\r\n+rUZ3QKBgQD3XGwp6Q72wC/b54oHFQSF4dQUelDPiSahljkWm6NRgqxK9NDw7Npn\r\nVLeGgVfA8Z3tvpXSggJmEA1VNt89NKrjzDrcvcxTzHV/gImNyBpisfsVHQrdghob\r\nfzXxPz3vVIjMLGEYpWumd/nnReWuhcC2rUdo/S0Wc7+CABs7B3UdZg==\r\n-----END RSA PRIVATE KEY-----"
  }
}

You don’t need to embed the PEM content directly in the terraform script. You can use the file function or the local_file provider for that. Using the file function, the declaration will be much cleaner, as below:

resource "authlete_service" "rsa" {
  issuer                        = "https://test.com"
  service_name                  = "RSA Test API"
  supported_grant_types         = ["AUTHORIZATION_CODE", "REFRESH_TOKEN"]
  supported_response_types      = ["CODE"]
  access_token_sign_alg         = "RS256"
  access_token_signature_key_id = "rsa1"
  jwk {
    kid             = "rsa1"
    alg             = "RS256"
    use             = "sig"
    pem_private_key = file("op_sign_key.pem")
  }
}

If you are using the Hashicorp TLS provider, you can simply provide the reference to the private key like below and let the provider manage the private key.

tls_private_key.example.private_key_pem

resource "tls_private_key" "example" {
  algorithm = "ECDSA"
}

resource "authlete_service" "rsa" {
  issuer                        = "https://test.com"
  service_name                  = "RSA Test API"
  supported_grant_types         = ["AUTHORIZATION_CODE", "REFRESH_TOKEN"]
  supported_response_types      = ["CODE"]
  access_token_sign_alg         = "RS256"
  access_token_signature_key_id = "rsa1"
  jwk {
    kid             = "rsa1"
    alg             = "RS256"
    use             = "sig"
    pem_private_key = tls_private_key.example.private_key_pem
  }
}

Key Rotation

There are 3 concerns that key rotation needs to address:

  • keys are required to be considered weaker as time goes by and tokens are signed or encrypted using it,
  • the clients cache the JWK responses (usually not respecting the Cache-Control header directive),
  • the valid tokens will not become invalid just after the keys are changed.

Best practice for managing key rotations are:

  • establish a time-to-live of the key and enforce it,
  • introduce the new key with proper time for clients that have that in cache,
  • change the key used,
  • after reasonable time, remove the previous key.

Check if you can assume a fair usage of the JWK endpoint. Usually one week is a reasonable time window, but your clients might have a static cache of the keys.

The time to remove the key used is a function of how long the tokens (id, access, and refresh tokens) are valid. When the refresh tokens are very long-lived, like weeks, you should consider using opaque refresh token and in the case of JWT based refresh token, another key should be used for signing it.

The mechanics of rotating those key would be like below. Note that for simplicity we are using one single key for signing every object, which should not be used in a production deployment.

% terraform apply --auto-approve

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # authlete_service.as will be created
  + resource "authlete_service" "as" {
      + access_token_sign_alg                 = "ES256"
      + access_token_signature_key_id         = "ec1"
      + access_token_type                     = (known after apply)
      + api_secret                            = (known after apply)
      + authorization_signature_key_id        = "ec1"
      + client_id_alias_enabled               = false
      + dcr_scope_used_as_requestable         = (known after apply)
      + description                           = "A terraform based service for managing the Authlete based OAuth server"
      + direct_authorization_endpoint_enabled = false
      + direct_introspection_endpoint_enabled = (known after apply)
      + direct_jwks_endpoint_enabled          = false
      + direct_revocation_endpoint_enabled    = (known after apply)
      + direct_token_endpoint_enabled         = (known after apply)
      + direct_user_info_endpoint_enabled     = false
      + id                                    = (known after apply)
      + id_token_signature_key_id             = "ec1"
      + ignore_port_loopback_redirect         = (known after apply)
      + issuer                                = "https://as.mydomain.com"
      + service_name                          = "MyDomainAS"
      + single_access_token_per_subject       = (known after apply)
      + supported_claim_types                 = (known after apply)
      + supported_displays                    = (known after apply)
      + supported_grant_types                 = [
          + "AUTHORIZATION_CODE",
        ]
      + supported_introspection_auth_methods  = (known after apply)
      + supported_response_types              = [
          + "CODE",
        ]
      + supported_revocation_auth_methods     = (known after apply)
      + supported_token_auth_methods          = (known after apply)
      + user_info_signature_key_id            = "ec1"

      + jwk {
          + alg      = "ES256"
          + crv      = "P-256"
          + generate = true
          + kid      = "ec1"
          + use      = "sig"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + api_key    = (known after apply)
  + api_secret = (sensitive value)
authlete_service.as: Creating...
authlete_service.as: Creation complete after 1s [id=8052816536756]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

api_key = "8052816536756"
api_secret = <sensitive>

Once the service is created, you can check the jwk response of the server with commmand below

% curl  "https://api.authlete.com/api/service/jwks/get?pretty=true" -u `terraform output -raw api_key`:`terraform output -raw api_secret`
{
  "keys": [
    {
      "kty": "EC",
      "use": "sig",
      "crv": "P-256",
      "kid": "ec1",
      "x": "ZesyKvoSRYqajx9CF_DbPmC6Nn-05FnJ1JaEbVlOrVY",
      "y": "jv7scys6qskxsW_Xlwtqqj1h_4g9ePeZ497oHEMLyAs",
      "alg": "ES256"
    }
  ]
}

Now, let’s introduce a new key by changing main.tf as below and applying the changes:

% cat main.tf
resource "authlete_service" "as" {
  issuer = "https://as.mydomain.com"
  service_name = "MyDomainAS"
  description = "A terraform based service for managing the Authlete based OAuth server"
  supported_grant_types = ["AUTHORIZATION_CODE"]
  supported_response_types = ["CODE"]
  access_token_sign_alg = "ES256"
  access_token_signature_key_id = "ec1"
  id_token_signature_key_id = "ec1"
  user_info_signature_key_id = "ec1"
  authorization_signature_key_id = "ec1"

  jwk {
    kid = "ec1"
    alg = "ES256"
    use = "sig"
    crv = "P-256"
    generate = true
  }

# second step is to include a new key
  jwk {
    kid = "ec2"
    alg = "ES256"
    use = "sig"
    crv = "P-256"
    generate = true
  }

}
% terraform apply --auto-approve
authlete_service.as: Refreshing state... [id=8052816536756]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # authlete_service.as will be updated in-place
  ~ resource "authlete_service" "as" {
        id                                            = "8052816536756"
        # (71 unchanged attributes hidden)

      + jwk {
          + alg      = "ES256"
          + crv      = "P-256"
          + generate = true
          + kid      = "ec2"
          + use      = "sig"
        }
        # (1 unchanged block hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.
authlete_service.as: Modifying... [id=8052816536756]
authlete_service.as: Modifications complete after 1s [id=8052816536756]
│ Warning: Updating JWK
│   with authlete_service.as,
│   on main.tf line 5, in resource "authlete_service" "as":
│    5: resource "authlete_service" "as" {
│ Updating JWK

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

Outputs:

api_key = "8052816536756"
api_secret = <sensitive>

Now if we check the JWK response the new key (or set of keys in your case) should be presented.

% curl  "https://api.authlete.com/api/service/jwks/get?pretty=true" -u `terraform output -raw api_key`:`terraform output -raw api_secret`
{
  "keys": [
    {
      "kty": "EC",
      "use": "sig",
      "crv": "P-256",
      "kid": "ec1",
      "x": "ZesyKvoSRYqajx9CF_DbPmC6Nn-05FnJ1JaEbVlOrVY",
      "y": "jv7scys6qskxsW_Xlwtqqj1h_4g9ePeZ497oHEMLyAs",
      "alg": "ES256"
    },
    {
      "kty": "EC",
      "use": "sig",
      "crv": "P-256",
      "kid": "ec2",
      "x": "w7OgBjdykXc_53s9N2AnNm4kYuawYjm3lR2rwZE-wDM",
      "y": "NEJ4EtrQDfaQOG3xMXN9img9HAyjNeBfq4JM4nGCiW4",
      "alg": "ES256"
    }
  ]
}

Given enough time for the clients to pick up the new JWK content, we can switch the active key by changing the main.tf as below:

% cat main.tf
resource "authlete_service" "as" {
  issuer = "https://as.mydomain.com"
  service_name = "MyDomainAS"
  description = "A terraform based service for managing the Authlete based OAuth server"
  supported_grant_types = ["AUTHORIZATION_CODE"]
  supported_response_types = ["CODE"]
  access_token_sign_alg = "ES256"
  access_token_signature_key_id = "ec2"
  id_token_signature_key_id = "ec2"
  user_info_signature_key_id = "ec2"
  authorization_signature_key_id = "ec2"

  jwk {
    kid = "ec1"
    alg = "ES256"
    use = "sig"
    crv = "P-256"
    generate = true
  }

# second step is to include a new key
  jwk {
    kid = "ec2"
    alg = "ES256"
    use = "sig"
    crv = "P-256"
    generate = true
  }

}
% terraform apply --auto-approve
authlete_service.as: Refreshing state... [id=8052816536756]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # authlete_service.as will be updated in-place
  ~ resource "authlete_service" "as" {
      ~ access_token_signature_key_id                 = "ec1" -> "ec2"
      ~ authorization_signature_key_id                = "ec1" -> "ec2"
        id                                            = "8052816536756"
      ~ id_token_signature_key_id                     = "ec1" -> "ec2"
      ~ user_info_signature_key_id                    = "ec1" -> "ec2"
        # (67 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.
authlete_service.as: Modifying... [id=8052816536756]
authlete_service.as: Modifications complete after 1s [id=8052816536756]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

Outputs:

api_key = "8052816536756"
api_secret = <sensitive>

After the last command, Authlete will use the key ec2 to sign tokens, but will still consider tokens signed by ec1 as valid.

When the time has come for dropping support for tokens signed by ec1, just go ahead and delete the entry as below:

% cat main.tf
provider "authlete" {

}

resource "authlete_service" "as" {
  issuer = "https://as.mydomain.com"
  service_name = "MyDomainAS"
  description = "A terraform based service for managing the Authlete based OAuth server"
  supported_grant_types = ["AUTHORIZATION_CODE"]
  supported_response_types = ["CODE"]
  access_token_sign_alg = "ES256"
  access_token_signature_key_id = "ec2"
  id_token_signature_key_id = "ec2"
  user_info_signature_key_id = "ec2"
  authorization_signature_key_id = "ec2"

  jwk {
    kid = "ec2"
    alg = "ES256"
    use = "sig"
    crv = "P-256"
    generate = true
  }

}
% terraform apply --auto-approve
authlete_service.as: Refreshing state... [id=8052816536756]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # authlete_service.as will be updated in-place
  ~ resource "authlete_service" "as" {
        id                                            = "8052816536756"
        # (71 unchanged attributes hidden)

      ~ jwk {
          ~ kid      = "ec1" -> "ec2"
            # (6 unchanged attributes hidden)
        }
      - jwk {
          - alg      = "ES256" -> null
          - crv      = "P-256" -> null
          - generate = true -> null
          - key_size = 0 -> null
          - kid      = "ec2" -> null
          - use      = "sig" -> null
          - x5c      = [] -> null
        }
    }

Plan: 0 to add, 1 to change, 0 to destroy.
authlete_service.as: Modifying... [id=8052816536756]
authlete_service.as: Modifications complete after 1s [id=8052816536756]
│ Warning: Updating JWK
│   with authlete_service.as,
│   on main.tf line 5, in resource "authlete_service" "as":
│    5: resource "authlete_service" "as" {
│ Updating JWK

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

Outputs:

api_key = "8052816536756"
api_secret = <sensitive>

After this apply command the ec1 key is gone from JWK and the tokens signed by that key are not valid.

% curl  "https://api.authlete.com/api/service/jwks/get?pretty=true" -u `terraform output -raw api_key`:`terraform output -raw api_secret`
{
  "keys": [
    {
      "kty": "EC",
      "use": "sig",
      "crv": "P-256",
      "kid": "ec2",
      "x": "w7OgBjdykXc_53s9N2AnNm4kYuawYjm3lR2rwZE-wDM",
      "y": "NEJ4EtrQDfaQOG3xMXN9img9HAyjNeBfq4JM4nGCiW4",
      "alg": "ES256"
    }
  ]
}%

Please note that the same process should be applied for RSA or EC keys.

Next Step