AJ ONeal
5 years ago
3 changed files with 274 additions and 0 deletions
@ -0,0 +1,187 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"crypto/ecdsa" |
|||
"crypto/elliptic" |
|||
"crypto/rand" |
|||
"crypto/sha256" |
|||
"encoding/base64" |
|||
"encoding/json" |
|||
"flag" |
|||
"fmt" |
|||
"log" |
|||
"math/big" |
|||
"net/http" |
|||
"os" |
|||
"strconv" |
|||
"time" |
|||
) |
|||
|
|||
type PrivateJWK struct { |
|||
PublicJWK |
|||
D string `json:"d"` |
|||
} |
|||
type PublicJWK struct { |
|||
Crv string `json:"crv"` |
|||
X string `json:"x"` |
|||
Y string `json:"y"` |
|||
} |
|||
|
|||
func main() { |
|||
done := make(chan bool) |
|||
var port int |
|||
var host string |
|||
|
|||
jwkm := map[string]string{ |
|||
"crv": "P-256", |
|||
"d": "GYAwlBHc2mPsj1lp315HbYOmKNJ7esmO3JAkZVn9nJs", |
|||
"x": "ToL2HppsTESXQKvp7ED6NMgV4YnwbMeONexNry3KDNQ", |
|||
"y": "Tt6Q3rxU37KAinUV9PLMlwosNy1t3Bf2VDg5q955AGc", |
|||
} |
|||
jwk := &PrivateJWK{ |
|||
PublicJWK: PublicJWK{ |
|||
Crv: jwkm["crv"], |
|||
X: jwkm["x"], |
|||
Y: jwkm["y"], |
|||
}, |
|||
D: jwkm["d"], |
|||
} |
|||
priv := parseKey(jwk) |
|||
pub := &priv.PublicKey |
|||
thumbprint := thumbprintKey(pub) |
|||
|
|||
portFlag := flag.Int("port", 0, "Port on which the HTTP server should run") |
|||
urlFlag := flag.String("url", "", "Outward-facing address, such as https://example.com") |
|||
flag.Parse() |
|||
|
|||
if nil != portFlag && *portFlag > 0 { |
|||
port = *portFlag |
|||
} else { |
|||
portStr := os.Getenv("PORT") |
|||
port, _ = strconv.Atoi(portStr) |
|||
} |
|||
if port < 1 { |
|||
fmt.Fprintf(os.Stderr, "You must specify --port or PORT\n") |
|||
os.Exit(1) |
|||
} |
|||
|
|||
if nil != urlFlag && "" != *urlFlag { |
|||
host = *urlFlag |
|||
} else { |
|||
host = "http://localhost:" + strconv.Itoa(port) |
|||
} |
|||
|
|||
http.HandleFunc("/access_token", func(w http.ResponseWriter, r *http.Request) { |
|||
log.Printf("%s %s\n", r.Method, r.URL.Path) |
|||
var scheme string |
|||
if nil != r.TLS || "https" == r.Header.Get("X-Forwarded-Proto") { |
|||
scheme = "https://" |
|||
} else { |
|||
scheme = "http://" |
|||
} |
|||
_, _, token := genToken(scheme + r.Host, priv) |
|||
fmt.Fprintf(w, token) |
|||
}) |
|||
http.HandleFunc("/key.jwk.json", func(w http.ResponseWriter, r *http.Request) { |
|||
log.Printf("%s %s", r.Method, r.URL.Path) |
|||
fmt.Fprintf(w, `{ "kty": "EC" , "crv": %q , "d": %q , "x": %q , "y": %q , "ext": true , "key_ops": ["sign"] }`, jwk.Crv, jwk.D, jwk.X, jwk.Y) |
|||
}) |
|||
http.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) { |
|||
var scheme string |
|||
if nil != r.TLS || "https" == r.Header.Get("X-Forwarded-Proto") { |
|||
scheme = "https://" |
|||
} else { |
|||
scheme = "http://" |
|||
} |
|||
log.Printf("%s %s\n", r.Method, r.URL.Path) |
|||
fmt.Fprintf(w, `{ "issuer": "%s", "jwks_uri": "%s/.well-known/jwks.json" }`, scheme+r.Host, scheme+r.Host) |
|||
}) |
|||
http.HandleFunc("/.well-known/jwks.json", func(w http.ResponseWriter, r *http.Request) { |
|||
log.Printf("%s %s", r.Method, r.URL.Path) |
|||
jwkstr := fmt.Sprintf( |
|||
`{ "keys": [ { "kty": "EC" , "crv": %q , "x": %q , "y": %q , "kid": %q , "ext": true , "key_ops": ["verify"] , "exp": %s } ] }`, |
|||
jwk.Crv, jwk.X, jwk.Y, thumbprint, strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10), |
|||
) |
|||
|
|||
fmt.Println(jwkstr) |
|||
fmt.Fprintf(w, jwkstr) |
|||
}) |
|||
fs := http.FileServer(http.Dir("public")) |
|||
http.Handle("/", fs) |
|||
/* |
|||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
|||
log.Printf(r.Method, r.URL.Path) |
|||
http.Error(w, "Not Found", http.StatusNotFound) |
|||
}) |
|||
*/ |
|||
|
|||
fmt.Printf("Serving on port %d\n", port) |
|||
go func() { |
|||
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), nil)) |
|||
done <- true |
|||
}() |
|||
|
|||
b, _ := json.Marshal(jwk) |
|||
fmt.Printf("Private Key:\n\t%s\n", string(b)) |
|||
b, _ = json.Marshal(jwk.PublicJWK) |
|||
fmt.Printf("Public Key:\n\t%s\n", string(b)) |
|||
protected, payload, token := genToken(host, priv) |
|||
fmt.Printf("Protected (Header):\n\t%s\n", protected) |
|||
fmt.Printf("Payload (Claims):\n\t%s\n", payload) |
|||
fmt.Printf("Access Token:\n\t%s\n", token) |
|||
|
|||
<-done |
|||
} |
|||
|
|||
func genToken(host string, priv *ecdsa.PrivateKey) (string, string, string) { |
|||
thumbprint := thumbprintKey(&priv.PublicKey) |
|||
protected := fmt.Sprintf(`{"typ":"JWT","alg":"ES256","kid":"%s"}`, thumbprint) |
|||
protected64 := base64.RawURLEncoding.EncodeToString([]byte(protected)) |
|||
payload := fmt.Sprintf( |
|||
`{"iss":"%s/","sub":"dummy","exp":%s}`, |
|||
host, strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10), |
|||
) |
|||
payload64 := base64.RawURLEncoding.EncodeToString([]byte(payload)) |
|||
hash := sha256.Sum256([]byte(fmt.Sprintf(`%s.%s`, protected64, payload64))) |
|||
r, s, _ := ecdsa.Sign(rand.Reader, priv, hash[:]) |
|||
rb := r.Bytes() |
|||
for len(rb) < 32 { |
|||
rb = append([]byte{0}, rb...) |
|||
} |
|||
sb := s.Bytes() |
|||
for len(rb) < 32 { |
|||
sb = append([]byte{0}, sb...) |
|||
} |
|||
sig64 := base64.RawURLEncoding.EncodeToString(append(rb, sb...)) |
|||
token := fmt.Sprintf(`%s.%s.%s`, protected64, payload64, sig64) |
|||
return protected, payload, token |
|||
} |
|||
|
|||
func parseKey(jwk *PrivateJWK) *ecdsa.PrivateKey { |
|||
xb, _ := base64.RawURLEncoding.DecodeString(jwk.X) |
|||
xi := &big.Int{} |
|||
xi.SetBytes(xb) |
|||
yb, _ := base64.RawURLEncoding.DecodeString(jwk.Y) |
|||
yi := &big.Int{} |
|||
yi.SetBytes(yb) |
|||
pub := &ecdsa.PublicKey{ |
|||
Curve: elliptic.P256(), |
|||
X: xi, |
|||
Y: yi, |
|||
} |
|||
|
|||
db, _ := base64.RawURLEncoding.DecodeString(jwk.D) |
|||
di := &big.Int{} |
|||
di.SetBytes(db) |
|||
priv := &ecdsa.PrivateKey{ |
|||
PublicKey: *pub, |
|||
D: di, |
|||
} |
|||
return priv |
|||
} |
|||
|
|||
func thumbprintKey(pub *ecdsa.PublicKey) string { |
|||
minpub := []byte(fmt.Sprintf(`{"crv":%q,"kty":"EC","x":%q,"y":%q}`, "P-256", pub.X, pub.Y)) |
|||
sha := sha256.Sum256(minpub) |
|||
return base64.RawURLEncoding.EncodeToString(sha[:]) |
|||
} |
@ -0,0 +1,85 @@ |
|||
<pre><code> |
|||
<h1>Tokens for Testing</h1> |
|||
Compatible with |
|||
|
|||
* OAuth2 |
|||
* OpenID Connect (OIDC) |
|||
* JOSE |
|||
* JWT |
|||
* JWK (Signed Access Tokens) |
|||
* JWS |
|||
|
|||
<h1>Resources</h1> |
|||
|
|||
* https://mock.pocketid.app/access_token |
|||
* https://xxx.mock.pocketid.app/.well-known/openid-configuration |
|||
* https://xxx.mock.pocketid.app/.well-known/jwks.json |
|||
* https://mock.pocketid.app/key.jwk.json |
|||
|
|||
<h2>Get a token</h2> |
|||
|
|||
Get a verifiable access token |
|||
|
|||
https://mock.pocketid.app/access_token |
|||
|
|||
For example: |
|||
|
|||
TOKEN=$(curl -fL https://mock.pocketid.app/access_token) |
|||
|
|||
<h3>The Token, Decoded</h3> |
|||
|
|||
The Token will look like this: |
|||
|
|||
<font |
|||
color="red">eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlU1Ym0tQUxtSVFOQ3hfMlFPT1piSm5Ec0d1aEd4WkRnV053alNqck5IbzQifQ</font>.<font |
|||
color="blue">eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwODAvIiwic3ViIjoiZHVtbXkiLCJleHAiOjE1NjI4NzEyNTh9</font>.<font |
|||
color="green">PWK2eTDQcVCpT_weogKuh4bHInnjqY6TGSnQzwEIc133WnJ1eUmLjq799COxSW7Dr6Khm1Po-CAXGVwCADIBEw</font> |
|||
|
|||
The <font color="red">first part</font> is the JWS protected header. |
|||
|
|||
Decoded, it looks like this: |
|||
|
|||
{"typ":"JWT","alg":"ES256","kid":"U5bm-ALmIQNCx_2QOOZbJnDsGuhGxZDgWNwjSjrNHo4"} |
|||
|
|||
The <font color="blue">second part</font> is the JWS payload of JWT claims. |
|||
|
|||
Decoded, it looks like this: |
|||
|
|||
{"iss":"http://localhost:4080/","sub":"dummy","exp":1562871258} |
|||
|
|||
The <font color="green">third part</font> is the signature. |
|||
|
|||
It can't be "decoded", per se. It's two positive 32-bit BigInts called <em>R</em> and <em>S</em>, padded with zeros to fill out all the bytes. |
|||
|
|||
<h2>Validate the Issuer</h2> |
|||
|
|||
The token be signed and verifiable. |
|||
|
|||
If the token issuer (iss) is "https://mock.pocketid.app/" |
|||
|
|||
* the key url (jwks_uri) will be found at "https://mock.pocketid.app/.well-known/openid-configuration" |
|||
* which will point to "https://mock.pocketid.app/.well-known/jwks.json" |
|||
* where there will be a key whose thumbprint (kid) matches that in the token |
|||
|
|||
The public key found there will look like this: |
|||
|
|||
{ "crv":"P-256" |
|||
, "x":"ToL2HppsTESXQKvp7ED6NMgV4YnwbMeONexNry3KDNQ" |
|||
, "y":"Tt6Q3rxU37KAinUV9PLMlwosNy1t3Bf2VDg5q955AGc" |
|||
} |
|||
|
|||
<h3>Get the Private Key</h3> |
|||
|
|||
In truth, there's only one private key right now. |
|||
|
|||
https://mock.pocketid.app/key.jwk.json |
|||
|
|||
You shouldn't use it for automated testing, because it will change, but it looks like this: |
|||
|
|||
{ "crv":"P-256" |
|||
, "x":"ToL2HppsTESXQKvp7ED6NMgV4YnwbMeONexNry3KDNQ" |
|||
, "y":"Tt6Q3rxU37KAinUV9PLMlwosNy1t3Bf2VDg5q955AGc" |
|||
, "d":"GYAwlBHc2mPsj1lp315HbYOmKNJ7esmO3JAkZVn9nJs" |
|||
} |
|||
|
|||
</code></pre> |
Loading…
Reference in new issue