Skip to content

Commit 8c4f7dc

Browse files
authored
Merge pull request #100 from enniosousa/master
feat: Add support to OIDC PEM certificate
2 parents e41c0e3 + ae4c5f7 commit 8c4f7dc

File tree

7 files changed

+238
-33
lines changed

7 files changed

+238
-33
lines changed

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ LABEL org.opencontainers.image.source=https://github.com/aertje/cloud-tasks-emul
1515

1616
WORKDIR /
1717

18+
COPY --from=builder /app/oidc.key oidc.key
19+
COPY --from=builder /app/oidc.cert oidc.cert
1820
COPY --from=builder /app/emulator .
1921
COPY --from=builder /app/emulator_from_env.sh .
2022
RUN chmod +x emulator_from_env.sh

oidc.cert

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDTTCCAjWgAwIBAgIUF+fUW7F4N/YDl/Ok6o5ogVKstmgwDQYJKoZIhvcNAQEL
3+
BQAwNTEzMDEGA1UEAwwqb2lkYy5mZWRlcmF0ZWQtc2lnbm9uLmNsb3VkLXRhc2tz
4+
LWVtdWxhdG9yMCAXDTI0MDYyNTIwNTMwNloYDzIwNTExMTExMjA1MzA2WjA1MTMw
5+
MQYDVQQDDCpvaWRjLmZlZGVyYXRlZC1zaWdub24uY2xvdWQtdGFza3MtZW11bGF0
6+
b3IwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+EePjNlISDurX4F1J
7+
vNKK+2afgRYX89kgXuAAf7iKqbu/bYw37bC+eak0tAb/4t4nkzf2QMda3Z6LccSz
8+
E/FsR54dHMKbbcCBcMZOSO5RReLsY/WdCZZxmfJyQPSOvyRk7vz2lq5yTrUa+dCG
9+
12XiY/ckIJc8jR0m9uSvvqeL6EyeOkHsbIKESCUgCuyFM0/CEeb7ozRzhHe5W/NB
10+
Sm4TsIRyKw0fW7wczRo6dApdhzjZrc/jKWWkPvSM23TTxK1fLIgjA3gsVP37m8z0
11+
WsESljiT0QCCBTZHsUSh2eTLp7yCs9XZvTPZ5Eu7iOAhM8zPLKphzotxwQ+yf31e
12+
qQXXAgMBAAGjUzBRMB0GA1UdDgQWBBQM0EC+S8XGgwTe9cqXmJFGEaiuzjAfBgNV
13+
HSMEGDAWgBQM0EC+S8XGgwTe9cqXmJFGEaiuzjAPBgNVHRMBAf8EBTADAQH/MA0G
14+
CSqGSIb3DQEBCwUAA4IBAQAf36NX9kdGdBwQppY9lO5ElcxVbGg8RG8ieOFM86eg
15+
1TJ14I8tKBdR2wPd/N+diRhsnctrVGEXulgItGvZjIKjnoWwVi/sPte5WJcMoR3Y
16+
csiLHBCW9tL6O8NaZuchSoKxlkE/qk2R1QLZtBaGXOjKm1+vIhNzNcdrMKinIfze
17+
OqbKJ0UDNapy59o65Eix8gZoeIc70WICWn3yKHcAah7FIKAVw2yA11QyuYa2xB9h
18+
1SuHbUN8voSaFaNdF3GIHxktLB7UU1yz6WDxbz1dBmWNK7FxyeeWrtjBKikQDZZu
19+
hxrkYARnT2CySwuUk5IvxzTYeebRoBQzGD8SuTqIvCzZ
20+
-----END CERTIFICATE-----

oidc.go

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/base64"
66
"encoding/json"
77
"fmt"
8+
"io/ioutil"
89
"log"
910
"net/http"
1011
"net/url"
@@ -15,38 +16,7 @@ import (
1516
)
1617

1718
const jwksUriPath = "/jwks"
18-
19-
// This private key is, of course, not actually private!
20-
const openIdPrivateKeyStr = `
21-
-----BEGIN PRIVATE KEY-----
22-
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+EePjNlISDurX
23-
4F1JvNKK+2afgRYX89kgXuAAf7iKqbu/bYw37bC+eak0tAb/4t4nkzf2QMda3Z6L
24-
ccSzE/FsR54dHMKbbcCBcMZOSO5RReLsY/WdCZZxmfJyQPSOvyRk7vz2lq5yTrUa
25-
+dCG12XiY/ckIJc8jR0m9uSvvqeL6EyeOkHsbIKESCUgCuyFM0/CEeb7ozRzhHe5
26-
W/NBSm4TsIRyKw0fW7wczRo6dApdhzjZrc/jKWWkPvSM23TTxK1fLIgjA3gsVP37
27-
m8z0WsESljiT0QCCBTZHsUSh2eTLp7yCs9XZvTPZ5Eu7iOAhM8zPLKphzotxwQ+y
28-
f31eqQXXAgMBAAECggEAGqcbk7L8UzfwSpFVw49M3txeCaPqWzWAjv9+3dMLJ7ah
29-
cziDXxxfmnYo+hD8oklH6bjFMiznR6CoKNmtQYdcZVitnVt5Fp6PThdoV3X2pULt
30-
jUR/HqRHimqSCt9867919QlmQ5XhpHnQ/5VkXmQ6D0MBVvmS+5S2L86TRumvSPjt
31-
xkcsFryxMwyhHiv3Dx+Vqz0RcSWqBe3AJAEUCDsqXL8OMUOoyDcsD34iRQdV7O5m
32-
sjRzU+od9a5b3dLrY9ufrlkcvrn5SbDZPMfwMXvrH5Y+XpGLHAxsMjqktVBitesV
33-
njHiO57RQePbvtQ8sgxTLFe9sbPT51kI+R2urS7f8QKBgQD8oYxQ4NyjUB5SgQ02
34-
/KA5FLcDlkI6wQK5C2hMEmW7Q2+DRQ70KjoSdLVowkRuAk3MX3RxfRVLTq+Cgkjn
35-
dgW2msjqAqOjpZ6Smw01hjEbMMcVMrwRHjSWwG4vIGMaNQqVpzaAR9Pu48jCHyMX
36-
LnsdGbcD8L1jLcSDuE1ComJFRQKBgQDAmsQMoCBH824Q8PhTj2jH0hra+jZg1Tje
37-
42br87FtHovpfUjVYalCg4oiQWAqapeIbagjgA5eMqzf2JOFbu7VgebYr15v3Nc1
38-
WJzwMmE7fAojopo1fOYQ1HTddbvf3LTJcnwnAggcGq4ENysFcbfRD+ldTm1RLoLO
39-
Ny7yuwHqawKBgQCmZkYE88eAboI6d7RblpR2ZJWTcEJZbs47Ui81hByr9uQZg8Aw
40-
xSuRAnyG7wahqzTRO8J4ChqfismB3gzlIFDtERDrSie835cOG8Dck3H+5ecLqGpF
41-
oC6laURqGBwOpAc/wW7dmfIXdMPEUTwMxdnjtg9dMhGcpQW+eQOys0ClPQKBgCOP
42-
b4r1NYCTTUsLco3a+HmMLTEo6UlPlMRyL9p4j9WZwjNF0mCzO1DwgFx6vYqXS4sA
43-
0/5Z8k0qBgj+L55/MNFyvnBbUJBOsd1DkxY19wXIjQavStF9UezhjQImbp2SXj6j
44-
SJDbKywlMOPOW78Rk+KhkXCMvloywCvavGxMYropAoGBAK7ECAs0AZLlUPkXuYmL
45-
U1GzFKUl3xDgczMSof5nPJCHcUm0fl02883IhEFEBvzqo5fu8pIzKGKpVwrNud7E
46-
/cLTJUkejD5e0h4V5ykcTUs9yDrxopQ54NW0lj7Se00e5MAUH0SRwbjbFdzQ3AYd
47-
FSkhEKj2YXWlriv3hyPIC8Aq
48-
-----END PRIVATE KEY-----
49-
`
19+
const certsUriPath = "/certs"
5020

5121
var OpenIDConfig struct {
5222
IssuerURL string
@@ -62,7 +32,11 @@ type OpenIDConnectClaims struct {
6232

6333
func init() {
6434
var err error
65-
OpenIDConfig.PrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM([]byte(openIdPrivateKeyStr))
35+
openIdPrivateKeyStr2, err := ioutil.ReadFile("oidc.key")
36+
if err != nil {
37+
panic(err)
38+
}
39+
OpenIDConfig.PrivateKey, err = jwt.ParseRSAPrivateKeyFromPEM([]byte(openIdPrivateKeyStr2))
6640
if err != nil {
6741
panic(err)
6842
}
@@ -150,10 +124,25 @@ func openIDJWKSHttpHandler(w http.ResponseWriter, r *http.Request) {
150124
respondJSON(w, config, 24*time.Hour)
151125
}
152126

127+
func openIDCertsHttpHandler(w http.ResponseWriter, r *http.Request) {
128+
var err error
129+
openIdcert, err := ioutil.ReadFile("oidc.cert")
130+
if err != nil {
131+
panic(err)
132+
}
133+
134+
config := map[string]interface{}{
135+
OpenIDConfig.KeyID: string(openIdcert),
136+
}
137+
138+
respondJSON(w, config, 24*time.Hour)
139+
}
140+
153141
func serveOpenIDConfigurationEndpoint(listenAddr string, listenPort string) *http.Server {
154142
mux := http.NewServeMux()
155143
mux.HandleFunc("/.well-known/openid-configuration", openIDConfigHttpHandler)
156144
mux.HandleFunc(jwksUriPath, openIDJWKSHttpHandler)
145+
mux.HandleFunc(certsUriPath, openIDCertsHttpHandler)
157146

158147
server := &http.Server{Addr: listenAddr + ":" + listenPort, Handler: mux}
159148
go server.ListenAndServe()

oidc.key

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+EePjNlISDurX
3+
4F1JvNKK+2afgRYX89kgXuAAf7iKqbu/bYw37bC+eak0tAb/4t4nkzf2QMda3Z6L
4+
ccSzE/FsR54dHMKbbcCBcMZOSO5RReLsY/WdCZZxmfJyQPSOvyRk7vz2lq5yTrUa
5+
+dCG12XiY/ckIJc8jR0m9uSvvqeL6EyeOkHsbIKESCUgCuyFM0/CEeb7ozRzhHe5
6+
W/NBSm4TsIRyKw0fW7wczRo6dApdhzjZrc/jKWWkPvSM23TTxK1fLIgjA3gsVP37
7+
m8z0WsESljiT0QCCBTZHsUSh2eTLp7yCs9XZvTPZ5Eu7iOAhM8zPLKphzotxwQ+y
8+
f31eqQXXAgMBAAECggEAGqcbk7L8UzfwSpFVw49M3txeCaPqWzWAjv9+3dMLJ7ah
9+
cziDXxxfmnYo+hD8oklH6bjFMiznR6CoKNmtQYdcZVitnVt5Fp6PThdoV3X2pULt
10+
jUR/HqRHimqSCt9867919QlmQ5XhpHnQ/5VkXmQ6D0MBVvmS+5S2L86TRumvSPjt
11+
xkcsFryxMwyhHiv3Dx+Vqz0RcSWqBe3AJAEUCDsqXL8OMUOoyDcsD34iRQdV7O5m
12+
sjRzU+od9a5b3dLrY9ufrlkcvrn5SbDZPMfwMXvrH5Y+XpGLHAxsMjqktVBitesV
13+
njHiO57RQePbvtQ8sgxTLFe9sbPT51kI+R2urS7f8QKBgQD8oYxQ4NyjUB5SgQ02
14+
/KA5FLcDlkI6wQK5C2hMEmW7Q2+DRQ70KjoSdLVowkRuAk3MX3RxfRVLTq+Cgkjn
15+
dgW2msjqAqOjpZ6Smw01hjEbMMcVMrwRHjSWwG4vIGMaNQqVpzaAR9Pu48jCHyMX
16+
LnsdGbcD8L1jLcSDuE1ComJFRQKBgQDAmsQMoCBH824Q8PhTj2jH0hra+jZg1Tje
17+
42br87FtHovpfUjVYalCg4oiQWAqapeIbagjgA5eMqzf2JOFbu7VgebYr15v3Nc1
18+
WJzwMmE7fAojopo1fOYQ1HTddbvf3LTJcnwnAggcGq4ENysFcbfRD+ldTm1RLoLO
19+
Ny7yuwHqawKBgQCmZkYE88eAboI6d7RblpR2ZJWTcEJZbs47Ui81hByr9uQZg8Aw
20+
xSuRAnyG7wahqzTRO8J4ChqfismB3gzlIFDtERDrSie835cOG8Dck3H+5ecLqGpF
21+
oC6laURqGBwOpAc/wW7dmfIXdMPEUTwMxdnjtg9dMhGcpQW+eQOys0ClPQKBgCOP
22+
b4r1NYCTTUsLco3a+HmMLTEo6UlPlMRyL9p4j9WZwjNF0mCzO1DwgFx6vYqXS4sA
23+
0/5Z8k0qBgj+L55/MNFyvnBbUJBOsd1DkxY19wXIjQavStF9UezhjQImbp2SXj6j
24+
SJDbKywlMOPOW78Rk+KhkXCMvloywCvavGxMYropAoGBAK7ECAs0AZLlUPkXuYmL
25+
U1GzFKUl3xDgczMSof5nPJCHcUm0fl02883IhEFEBvzqo5fu8pIzKGKpVwrNud7E
26+
/cLTJUkejD5e0h4V5ykcTUs9yDrxopQ54NW0lj7Se00e5MAUH0SRwbjbFdzQ3AYd
27+
FSkhEKj2YXWlriv3hyPIC8Aq
28+
-----END PRIVATE KEY-----

oidc_internal_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ package main
22

33
import (
44
"context"
5+
"crypto/rsa"
6+
"crypto/x509"
57
"encoding/json"
8+
"encoding/pem"
9+
"fmt"
610
"net/http"
711
"net/http/httptest"
12+
"os"
813
"testing"
914
"time"
1015

@@ -113,6 +118,80 @@ func TestOpenIdJWKSHttpHandler(t *testing.T) {
113118
)
114119
}
115120

121+
func TestOpenIdCertsHttpHandler(t *testing.T) {
122+
OpenIDConfig.KeyID = "any-key-id"
123+
124+
resp := performRequest("GET", "/certs", openIDCertsHttpHandler)
125+
126+
assert.Equal(t, http.StatusOK, resp.Code)
127+
128+
var err error
129+
130+
expires, err := time.Parse(http.TimeFormat, resp.Result().Header.Get("Expires"))
131+
require.NoError(t, err)
132+
assertRoughTimestamp(t, 24*time.Hour, expires.Unix(), "Expect future expires")
133+
134+
openIdcert, err := os.ReadFile("oidc.cert")
135+
require.NoError(t, err)
136+
137+
certs := map[string]interface{}{
138+
OpenIDConfig.KeyID: string(openIdcert),
139+
}
140+
141+
certsJSON, err := json.Marshal(certs)
142+
require.NoError(t, err)
143+
assert.JSONEq(t, string(certsJSON), resp.Body.String())
144+
}
145+
146+
func TestValidateOIDCTokenWithCertPem(t *testing.T) {
147+
var err error
148+
149+
tokenStr := createOIDCToken("foobar@service.com", "http://my.service/foo?bar=v", "http://my.api")
150+
151+
openIdcert, err := os.ReadFile("oidc.cert")
152+
require.NoError(t, err)
153+
154+
parsePEMCert := func(pemCert []byte) (*rsa.PublicKey, error) {
155+
block, _ := pem.Decode(pemCert)
156+
if block == nil || block.Type != "CERTIFICATE" {
157+
return nil, fmt.Errorf("failed to decode PEM block containing certificate")
158+
}
159+
160+
cert, err := x509.ParseCertificate(block.Bytes)
161+
if err != nil {
162+
return nil, fmt.Errorf("failed to parse certificate: %v", err)
163+
}
164+
165+
rsaPub, ok := cert.PublicKey.(*rsa.PublicKey)
166+
if !ok {
167+
return nil, fmt.Errorf("not an RSA public key")
168+
}
169+
170+
return rsaPub, nil
171+
}
172+
173+
// Load the public key
174+
pubKey, err := parsePEMCert(openIdcert)
175+
require.NoError(t, err)
176+
177+
// Parse the token
178+
parser := new(jwt.Parser)
179+
_, _, err = parser.ParseUnverified(tokenStr, &jwt.MapClaims{})
180+
require.NoError(t, err)
181+
182+
// Verify the token
183+
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
184+
// Validate the algorithm - this is optional but recommended
185+
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
186+
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
187+
}
188+
return pubKey, nil
189+
})
190+
require.NoError(t, err)
191+
192+
assert.True(t, token.Valid, "Token is valid")
193+
}
194+
116195
func TestConfigureOpenIdIssuerRejectsInvalidUrl(t *testing.T) {
117196
var err error
118197
_, err = configureOpenIdIssuer("junk")

readme-owncert.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
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:
2+
3+
1. **Generate a Private Key:**
4+
Use OpenSSL to generate a new private key file. Here’s how you can do it:
5+
6+
```bash
7+
openssl genpkey -algorithm RSA -out oidc.key
8+
```
9+
10+
This command generates an RSA private key and saves it to `oidc.key`.
11+
12+
2. **Generate a Self-Signed Certificate:**
13+
Once you have the private key, you can generate a self-signed certificate using the `req` command, as you've shown:
14+
15+
```bash
16+
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"
17+
```
18+
19+
- `-new`: Generate a new certificate request.
20+
- `-x509`: Create a self-signed certificate.
21+
- `-key oidc.key`: Use the private key file `oidc.key`.
22+
- `-out oidc.cert`: Output the certificate to `oidc.cert`.
23+
- `-days 10000`: Validity of the certificate in days.
24+
- `-subj "/CN=oidc.federated-signon.cloud-tasks-emulator"`: Subject of the certificate. Adjust the Common Name (CN) as needed.
25+
- `-config "path/to/openssl.cnf"`: Path to your OpenSSL configuration file. Adjust this path according to your setup.
26+
27+
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.

readme.MD

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ With this flag:
113113
will be available at `http://localhost:8980/.well-known/openid-configuration`
114114
* The emulator's public key(s) (in JWK format) will be available at
115115
`http://localhost:8980/jwks`
116+
* The emulator's public key(s) (in PEM format) will be available at
117+
`http://localhost:8980/certs`
116118

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

126+
Optionally, if you wish, you can use your own private key and self-signed certificate to
127+
sign the tokens. Here's how you can do it: [`readme-owncert.md`](./readme-owncert.md).
128+
129+
124130
## Flushing task state
125131

126132
By default, the emulator tracks the names of every task created since the emulator launched. The list
@@ -268,4 +274,58 @@ await client.createTask({
268274
parent: queueName,
269275
task: { httpRequest: { httpMethod: 'POST', url: 'https://www.google.com' } },
270276
});
277+
278+
// create task with OIDC token
279+
const payload = { foo: "bar" };
280+
const serviceAccountEmail = "account@project_id.iam.gserviceaccount.com"
281+
await client.createTask({
282+
parent: queueName,
283+
task: {
284+
httpRequest: {
285+
url: "https://myapp.example.com/worker",
286+
httpMethod: "POST",
287+
body: Buffer.from(JSON.stringify(payload)).toString("base64"),
288+
headers: {"Content-Type": "application/json"},
289+
oidcToken: {
290+
serviceAccountEmail,
291+
},
292+
},
293+
},
294+
});
271295
```
296+
297+
Receiving HTTP calls from the emulator and verifying OIDC tokens.
298+
```js
299+
// at this point you started the emulator with the -openid-issuer flag
300+
// and created a http task with oidc token
301+
// in this example we are assuming that the issuer is http://localhost:8980
302+
import { OAuth2Client } from "google-auth-library";
303+
304+
const client = new OAuth2Client({
305+
endpoints: {
306+
// PEM certs for node.js environment
307+
oauth2FederatedSignonPemCertsUrl: "http://localhost:8980/certs",
308+
309+
// JWK certs for browser environment
310+
oauth2FederatedSignonJwkCertsUrl: "http://localhost:8980/jwks",
311+
},
312+
issuers: ["http://localhost:8980"],
313+
});
314+
315+
// function using node.js
316+
// to handling the http request
317+
// to https://myapp.example.com/worker
318+
// the is webhook used in task creation
319+
// that is protected by oidc token
320+
function httpRequestHandler() {
321+
322+
// data from Authorization header
323+
const idToken = "...";
324+
325+
const ticket = await client.verifyIdToken({
326+
idToken,
327+
audience: "https://myapp.example.com/worker",
328+
});
329+
const payload = ticket.getPayload();
330+
console.info("Payload", payload);
331+
}

0 commit comments

Comments
 (0)