Skip to content

Commit

Permalink
Merge pull request #100 from enniosousa/master
Browse files Browse the repository at this point in the history
feat: Add support to OIDC PEM certificate
  • Loading branch information
aertje authored Dec 10, 2024
2 parents e41c0e3 ae4c5f7 commit 8c4f7dc
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 33 deletions.
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 15,8 @@ LABEL org.opencontainers.image.source=https://github.com/aertje/cloud-tasks-emul

WORKDIR /

COPY --from=builder /app/oidc.key oidc.key
COPY --from=builder /app/oidc.cert oidc.cert
COPY --from=builder /app/emulator .
COPY --from=builder /app/emulator_from_env.sh .
RUN chmod x emulator_from_env.sh
Expand Down
20 changes: 20 additions & 0 deletions oidc.cert
Original file line number Diff line number Diff line change
@@ -0,0 1,20 @@
-----BEGIN CERTIFICATE-----
MIIDTTCCAjWgAwIBAgIUF fUW7F4N/YDl/Ok6o5ogVKstmgwDQYJKoZIhvcNAQEL
BQAwNTEzMDEGA1UEAwwqb2lkYy5mZWRlcmF0ZWQtc2lnbm9uLmNsb3VkLXRhc2tz
LWVtdWxhdG9yMCAXDTI0MDYyNTIwNTMwNloYDzIwNTExMTExMjA1MzA2WjA1MTMw
MQYDVQQDDCpvaWRjLmZlZGVyYXRlZC1zaWdub24uY2xvdWQtdGFza3MtZW11bGF0
b3IwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC EePjNlISDurX4F1J
vNKK 2afgRYX89kgXuAAf7iKqbu/bYw37bC eak0tAb/4t4nkzf2QMda3Z6LccSz
E/FsR54dHMKbbcCBcMZOSO5RReLsY/WdCZZxmfJyQPSOvyRk7vz2lq5yTrUa dCG
12XiY/ckIJc8jR0m9uSvvqeL6EyeOkHsbIKESCUgCuyFM0/CEeb7ozRzhHe5W/NB
Sm4TsIRyKw0fW7wczRo6dApdhzjZrc/jKWWkPvSM23TTxK1fLIgjA3gsVP37m8z0
WsESljiT0QCCBTZHsUSh2eTLp7yCs9XZvTPZ5Eu7iOAhM8zPLKphzotxwQ yf31e
qQXXAgMBAAGjUzBRMB0GA1UdDgQWBBQM0EC S8XGgwTe9cqXmJFGEaiuzjAfBgNV
HSMEGDAWgBQM0EC S8XGgwTe9cqXmJFGEaiuzjAPBgNVHRMBAf8EBTADAQH/MA0G
CSqGSIb3DQEBCwUAA4IBAQAf36NX9kdGdBwQppY9lO5ElcxVbGg8RG8ieOFM86eg
1TJ14I8tKBdR2wPd/N diRhsnctrVGEXulgItGvZjIKjnoWwVi/sPte5WJcMoR3Y
csiLHBCW9tL6O8NaZuchSoKxlkE/qk2R1QLZtBaGXOjKm1 vIhNzNcdrMKinIfze
OqbKJ0UDNapy59o65Eix8gZoeIc70WICWn3yKHcAah7FIKAVw2yA11QyuYa2xB9h
1SuHbUN8voSaFaNdF3GIHxktLB7UU1yz6WDxbz1dBmWNK7FxyeeWrtjBKikQDZZu
hxrkYARnT2CySwuUk5IvxzTYeebRoBQzGD8SuTqIvCzZ
-----END CERTIFICATE-----
55 changes: 22 additions & 33 deletions oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 5,7 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
Expand All @@ -15,38 16,7 @@ import (
)

const jwksUriPath = "/jwks"

// This private key is, of course, not actually private!
const openIdPrivateKeyStr = `
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC EePjNlISDurX
4F1JvNKK 2afgRYX89kgXuAAf7iKqbu/bYw37bC eak0tAb/4t4nkzf2QMda3Z6L
ccSzE/FsR54dHMKbbcCBcMZOSO5RReLsY/WdCZZxmfJyQPSOvyRk7vz2lq5yTrUa
dCG12XiY/ckIJc8jR0m9uSvvqeL6EyeOkHsbIKESCUgCuyFM0/CEeb7ozRzhHe5
W/NBSm4TsIRyKw0fW7wczRo6dApdhzjZrc/jKWWkPvSM23TTxK1fLIgjA3gsVP37
m8z0WsESljiT0QCCBTZHsUSh2eTLp7yCs9XZvTPZ5Eu7iOAhM8zPLKphzotxwQ y
f31eqQXXAgMBAAECggEAGqcbk7L8UzfwSpFVw49M3txeCaPqWzWAjv9 3dMLJ7ah
cziDXxxfmnYo hD8oklH6bjFMiznR6CoKNmtQYdcZVitnVt5Fp6PThdoV3X2pULt
jUR/HqRHimqSCt9867919QlmQ5XhpHnQ/5VkXmQ6D0MBVvmS 5S2L86TRumvSPjt
xkcsFryxMwyhHiv3Dx Vqz0RcSWqBe3AJAEUCDsqXL8OMUOoyDcsD34iRQdV7O5m
sjRzU od9a5b3dLrY9ufrlkcvrn5SbDZPMfwMXvrH5Y XpGLHAxsMjqktVBitesV
njHiO57RQePbvtQ8sgxTLFe9sbPT51kI R2urS7f8QKBgQD8oYxQ4NyjUB5SgQ02
/KA5FLcDlkI6wQK5C2hMEmW7Q2 DRQ70KjoSdLVowkRuAk3MX3RxfRVLTq Cgkjn
dgW2msjqAqOjpZ6Smw01hjEbMMcVMrwRHjSWwG4vIGMaNQqVpzaAR9Pu48jCHyMX
LnsdGbcD8L1jLcSDuE1ComJFRQKBgQDAmsQMoCBH824Q8PhTj2jH0hra jZg1Tje
42br87FtHovpfUjVYalCg4oiQWAqapeIbagjgA5eMqzf2JOFbu7VgebYr15v3Nc1
WJzwMmE7fAojopo1fOYQ1HTddbvf3LTJcnwnAggcGq4ENysFcbfRD ldTm1RLoLO
Ny7yuwHqawKBgQCmZkYE88eAboI6d7RblpR2ZJWTcEJZbs47Ui81hByr9uQZg8Aw
xSuRAnyG7wahqzTRO8J4ChqfismB3gzlIFDtERDrSie835cOG8Dck3H 5ecLqGpF
oC6laURqGBwOpAc/wW7dmfIXdMPEUTwMxdnjtg9dMhGcpQW eQOys0ClPQKBgCOP
b4r1NYCTTUsLco3a HmMLTEo6UlPlMRyL9p4j9WZwjNF0mCzO1DwgFx6vYqXS4sA
0/5Z8k0qBgj L55/MNFyvnBbUJBOsd1DkxY19wXIjQavStF9UezhjQImbp2SXj6j
SJDbKywlMOPOW78Rk KhkXCMvloywCvavGxMYropAoGBAK7ECAs0AZLlUPkXuYmL
U1GzFKUl3xDgczMSof5nPJCHcUm0fl02883IhEFEBvzqo5fu8pIzKGKpVwrNud7E
/cLTJUkejD5e0h4V5ykcTUs9yDrxopQ54NW0lj7Se00e5MAUH0SRwbjbFdzQ3AYd
FSkhEKj2YXWlriv3hyPIC8Aq
-----END PRIVATE KEY-----
`
const certsUriPath = "/certs"

var OpenIDConfig struct {
IssuerURL string
Expand All @@ -62,7 32,11 @@ type OpenIDConnectClaims struct {

func init() {
var err error
OpenIDConfig.PrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM([]byte(openIdPrivateKeyStr))
openIdPrivateKeyStr2, err := ioutil.ReadFile("oidc.key")
if err != nil {
panic(err)
}
OpenIDConfig.PrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM([]byte(openIdPrivateKeyStr2))
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -150,10 124,25 @@ func openIDJWKSHttpHandler(w http.ResponseWriter, r *http.Request) {
respondJSON(w, config, 24*time.Hour)
}

func openIDCertsHttpHandler(w http.ResponseWriter, r *http.Request) {
var err error
openIdcert, err := ioutil.ReadFile("oidc.cert")
if err != nil {
panic(err)
}

config := map[string]interface{}{
OpenIDConfig.KeyID: string(openIdcert),
}

respondJSON(w, config, 24*time.Hour)
}

func serveOpenIDConfigurationEndpoint(listenAddr string, listenPort string) *http.Server {
mux := http.NewServeMux()
mux.HandleFunc("/.well-known/openid-configuration", openIDConfigHttpHandler)
mux.HandleFunc(jwksUriPath, openIDJWKSHttpHandler)
mux.HandleFunc(certsUriPath, openIDCertsHttpHandler)

server := &http.Server{Addr: listenAddr ":" listenPort, Handler: mux}
go server.ListenAndServe()
Expand Down
28 changes: 28 additions & 0 deletions oidc.key
Original file line number Diff line number Diff line change
@@ -0,0 1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC EePjNlISDurX
4F1JvNKK 2afgRYX89kgXuAAf7iKqbu/bYw37bC eak0tAb/4t4nkzf2QMda3Z6L
ccSzE/FsR54dHMKbbcCBcMZOSO5RReLsY/WdCZZxmfJyQPSOvyRk7vz2lq5yTrUa
dCG12XiY/ckIJc8jR0m9uSvvqeL6EyeOkHsbIKESCUgCuyFM0/CEeb7ozRzhHe5
W/NBSm4TsIRyKw0fW7wczRo6dApdhzjZrc/jKWWkPvSM23TTxK1fLIgjA3gsVP37
m8z0WsESljiT0QCCBTZHsUSh2eTLp7yCs9XZvTPZ5Eu7iOAhM8zPLKphzotxwQ y
f31eqQXXAgMBAAECggEAGqcbk7L8UzfwSpFVw49M3txeCaPqWzWAjv9 3dMLJ7ah
cziDXxxfmnYo hD8oklH6bjFMiznR6CoKNmtQYdcZVitnVt5Fp6PThdoV3X2pULt
jUR/HqRHimqSCt9867919QlmQ5XhpHnQ/5VkXmQ6D0MBVvmS 5S2L86TRumvSPjt
xkcsFryxMwyhHiv3Dx Vqz0RcSWqBe3AJAEUCDsqXL8OMUOoyDcsD34iRQdV7O5m
sjRzU od9a5b3dLrY9ufrlkcvrn5SbDZPMfwMXvrH5Y XpGLHAxsMjqktVBitesV
njHiO57RQePbvtQ8sgxTLFe9sbPT51kI R2urS7f8QKBgQD8oYxQ4NyjUB5SgQ02
/KA5FLcDlkI6wQK5C2hMEmW7Q2 DRQ70KjoSdLVowkRuAk3MX3RxfRVLTq Cgkjn
dgW2msjqAqOjpZ6Smw01hjEbMMcVMrwRHjSWwG4vIGMaNQqVpzaAR9Pu48jCHyMX
LnsdGbcD8L1jLcSDuE1ComJFRQKBgQDAmsQMoCBH824Q8PhTj2jH0hra jZg1Tje
42br87FtHovpfUjVYalCg4oiQWAqapeIbagjgA5eMqzf2JOFbu7VgebYr15v3Nc1
WJzwMmE7fAojopo1fOYQ1HTddbvf3LTJcnwnAggcGq4ENysFcbfRD ldTm1RLoLO
Ny7yuwHqawKBgQCmZkYE88eAboI6d7RblpR2ZJWTcEJZbs47Ui81hByr9uQZg8Aw
xSuRAnyG7wahqzTRO8J4ChqfismB3gzlIFDtERDrSie835cOG8Dck3H 5ecLqGpF
oC6laURqGBwOpAc/wW7dmfIXdMPEUTwMxdnjtg9dMhGcpQW eQOys0ClPQKBgCOP
b4r1NYCTTUsLco3a HmMLTEo6UlPlMRyL9p4j9WZwjNF0mCzO1DwgFx6vYqXS4sA
0/5Z8k0qBgj L55/MNFyvnBbUJBOsd1DkxY19wXIjQavStF9UezhjQImbp2SXj6j
SJDbKywlMOPOW78Rk KhkXCMvloywCvavGxMYropAoGBAK7ECAs0AZLlUPkXuYmL
U1GzFKUl3xDgczMSof5nPJCHcUm0fl02883IhEFEBvzqo5fu8pIzKGKpVwrNud7E
/cLTJUkejD5e0h4V5ykcTUs9yDrxopQ54NW0lj7Se00e5MAUH0SRwbjbFdzQ3AYd
FSkhEKj2YXWlriv3hyPIC8Aq
-----END PRIVATE KEY-----
79 changes: 79 additions & 0 deletions oidc_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 2,14 @@ package main

import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"

Expand Down Expand Up @@ -113,6 118,80 @@ func TestOpenIdJWKSHttpHandler(t *testing.T) {
)
}

func TestOpenIdCertsHttpHandler(t *testing.T) {
OpenIDConfig.KeyID = "any-key-id"

resp := performRequest("GET", "/certs", openIDCertsHttpHandler)

assert.Equal(t, http.StatusOK, resp.Code)

var err error

expires, err := time.Parse(http.TimeFormat, resp.Result().Header.Get("Expires"))
require.NoError(t, err)
assertRoughTimestamp(t, 24*time.Hour, expires.Unix(), "Expect future expires")

openIdcert, err := os.ReadFile("oidc.cert")
require.NoError(t, err)

certs := map[string]interface{}{
OpenIDConfig.KeyID: string(openIdcert),
}

certsJSON, err := json.Marshal(certs)
require.NoError(t, err)
assert.JSONEq(t, string(certsJSON), resp.Body.String())
}

func TestValidateOIDCTokenWithCertPem(t *testing.T) {
var err error

tokenStr := createOIDCToken("[email protected]", "http://my.service/foo?bar=v", "http://my.api")

openIdcert, err := os.ReadFile("oidc.cert")
require.NoError(t, err)

parsePEMCert := func(pemCert []byte) (*rsa.PublicKey, error) {
block, _ := pem.Decode(pemCert)
if block == nil || block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("failed to decode PEM block containing certificate")
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %v", err)
}

rsaPub, ok := cert.PublicKey.(*rsa.PublicKey)
if !ok {
return nil, fmt.Errorf("not an RSA public key")
}

return rsaPub, nil
}

// Load the public key
pubKey, err := parsePEMCert(openIdcert)
require.NoError(t, err)

// Parse the token
parser := new(jwt.Parser)
_, _, err = parser.ParseUnverified(tokenStr, &jwt.MapClaims{})
require.NoError(t, err)

// Verify the token
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
// Validate the algorithm - this is optional but recommended
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return pubKey, nil
})
require.NoError(t, err)

assert.True(t, token.Valid, "Token is valid")
}

func TestConfigureOpenIdIssuerRejectsInvalidUrl(t *testing.T) {
var err error
_, err = configureOpenIdIssuer("junk")
Expand Down
27 changes: 27 additions & 0 deletions readme-owncert.md
Original file line number Diff line number Diff line change
@@ -0,0 1,27 @@
You can create you own private key and self-signed certificate using OpenSSL to use in you own emulator. Here's how you can do it:

1. **Generate a Private Key:**
Use OpenSSL to generate a new private key file. Here’s how you can do it:

```bash
openssl genpkey -algorithm RSA -out oidc.key
```

This command generates an RSA private key and saves it to `oidc.key`.

2. **Generate a Self-Signed Certificate:**
Once you have the private key, you can generate a self-signed certificate using the `req` command, as you've shown:

```bash
openssl req -new -x509 -key oidc.key -out oidc.cert -days 10000 -subj "/CN=oidc.federated-signon.cloud-tasks-emulator" -config "path/to/openssl.cnf"
```

- `-new`: Generate a new certificate request.
- `-x509`: Create a self-signed certificate.
- `-key oidc.key`: Use the private key file `oidc.key`.
- `-out oidc.cert`: Output the certificate to `oidc.cert`.
- `-days 10000`: Validity of the certificate in days.
- `-subj "/CN=oidc.federated-signon.cloud-tasks-emulator"`: Subject of the certificate. Adjust the Common Name (CN) as needed.
- `-config "path/to/openssl.cnf"`: Path to your OpenSSL configuration file. Adjust this path according to your setup.

Make sure to replace `"path/to/openssl.cnf"` with the actual path to your OpenSSL configuration file on your system. This configuration file typically contains settings like default certificate extensions and other parameters relevant to certificate generation. Adjust the CN (Common Name) parameter (`/CN=`) to match your specific domain or server name.
60 changes: 60 additions & 0 deletions readme.MD
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 113,8 @@ With this flag:
will be available at `http://localhost:8980/.well-known/openid-configuration`
* The emulator's public key(s) (in JWK format) will be available at
`http://localhost:8980/jwks`
* The emulator's public key(s) (in PEM format) will be available at
`http://localhost:8980/certs`

The `-openid-issuer` URL can be any `http://hostname:port` value that your
application code can route to. The endpoint listens on `0.0.0.0` for easy
Expand All @@ -121,6 123,10 @@ use in docker / k8s environments.
You can, of course, export the content of the `/jwks` url if you prefer to
hardcode the public keys in your application.

Optionally, if you wish, you can use your own private key and self-signed certificate to
sign the tokens. Here's how you can do it: [`readme-owncert.md`](./readme-owncert.md).


## Flushing task state

By default, the emulator tracks the names of every task created since the emulator launched. The list
Expand Down Expand Up @@ -268,4 274,58 @@ await client.createTask({
parent: queueName,
task: { httpRequest: { httpMethod: 'POST', url: 'https://www.google.com' } },
});

// create task with OIDC token
const payload = { foo: "bar" };
const serviceAccountEmail = "account@project_id.iam.gserviceaccount.com"
await client.createTask({
parent: queueName,
task: {
httpRequest: {
url: "https://myapp.example.com/worker",
httpMethod: "POST",
body: Buffer.from(JSON.stringify(payload)).toString("base64"),
headers: {"Content-Type": "application/json"},
oidcToken: {
serviceAccountEmail,
},
},
},
});
```

Receiving HTTP calls from the emulator and verifying OIDC tokens.
```js
// at this point you started the emulator with the -openid-issuer flag
// and created a http task with oidc token
// in this example we are assuming that the issuer is http://localhost:8980
import { OAuth2Client } from "google-auth-library";

const client = new OAuth2Client({
endpoints: {
// PEM certs for node.js environment
oauth2FederatedSignonPemCertsUrl: "http://localhost:8980/certs",

// JWK certs for browser environment
oauth2FederatedSignonJwkCertsUrl: "http://localhost:8980/jwks",
},
issuers: ["http://localhost:8980"],
});

// function using node.js
// to handling the http request
// to https://myapp.example.com/worker
// the is webhook used in task creation
// that is protected by oidc token
function httpRequestHandler() {

// data from Authorization header
const idToken = "...";

const ticket = await client.verifyIdToken({
idToken,
audience: "https://myapp.example.com/worker",
});
const payload = ticket.getPayload();
console.info("Payload", payload);
}

0 comments on commit 8c4f7dc

Please sign in to comment.