diff --git a/.well-known/jwks.json b/.well-known/jwks.json new file mode 100644 index 0000000..cbbd748 --- /dev/null +++ b/.well-known/jwks.json @@ -0,0 +1,19 @@ +{ + "keys": [ + { + "crv": "P-256", + "kid": "tsb1m6h3xjp1HjXmJY_8pTtHxld5UvWxoPG9b2e_0aY", + "kty": "EC", + "use": "sig", + "x": "VSQ5P-nwzOMVYowySPF8-FFRTLGXfY611ErayGgM4cc", + "y": "qSVv69YEJdM7waa_w8wegsaFDZaFZNkRNV-2PUGXb_E" + }, + { + "e": "AQAB", + "kid": "dS08lAcJu_zmqIFkBwbI7rgi4OGlaF3uugjs6NysFEY", + "kty": "RSA", + "n": "q0yq8t-8Sw9nAJQAbDhiUMtxD_OEHigOekZrcLR38JkagqUZlxYZNp1B7NXM8GTymtz3qKzxUoI-mmE9gHq2nyDN8Jc_DTe_jnNFPD_bAxo92Ii_jpT74_6PR7I92BBvw0-ecxKHScJlO2tD2l1hxyOwpJ52Gt3WuXp2Ezsd3_14boTU4Z3Wh7WFNStz-BBwl09KR8UmVz1_pifJMnDEDXsRMEorFEbSDlJoZLAQgjAEwEZdmecH256WANKGylk1m5PWIBA59FMNXdQZIN1e6Cc0knaqZJHLor1hzmfSjyxxhSck0xk0HccUFNskS9QMoX05IvupxcnMBVPXIQBstw", + "use": "sig" + } + ] +} diff --git a/.well-known/openid-configuration b/.well-known/openid-configuration new file mode 100644 index 0000000..18af9ce --- /dev/null +++ b/.well-known/openid-configuration @@ -0,0 +1,4 @@ +{ + "issuer": "https://therootcompany.github.io/libauth/", + "jwks_uri": "https://therootcompany.github.io/libauth/.well-known/jwks.json" +} diff --git a/.well-known/openid-configuration.json b/.well-known/openid-configuration.json new file mode 120000 index 0000000..99ba1e9 --- /dev/null +++ b/.well-known/openid-configuration.json @@ -0,0 +1 @@ +openid-configuration \ No newline at end of file diff --git a/README.md b/README.md index 3212fed..963db38 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # [libauth](https://git.rootprojects.org/root/libauth) -LibAuth for Go - A modern authentication framework that feels as light as a library. +LibAuth for Go - A modern authentication framework that feels as light as a +library. [![godoc_button]][godoc] @@ -27,7 +28,7 @@ import ( func main() { r := chi.NewRouter() - whitelist, err := keyfetch.NewWhitelist([]string{"https://accounts.google.com"}) + whitelist, err := keyfetch.NewWhitelist([]string{"https://therootcompany.github.io/libauth/"}) if nil != err { panic(err) } @@ -53,11 +54,45 @@ func main() { } ``` +How to create a demo token with [keypairs][https://webinstall.dev/keypairs]: + +```bash +my_key='./examples/privkey.ec.jwk.json' +my_claims='{ + "iss": "https://therootcompany.github.io/libauth/", + "sub": "1", + "email_verified": false, + "email": "jo@example.com" +}' + +keypairs sign \ + --exp 1h \ + "${my_key}" \ + "${my_claims}" \ + > jwt.txt + 2> jws.json +``` + How to pass an auth token: ```bash -curl -X POST http://localhost:3000/api/users/profile \ - -H 'Authorization: Bearer ' \ - -H 'Content-Type: application/json' \ - --raw-data '{ "foo": "bar" }' +pushd ./examples +go run ./server.go ``` + +```bash +my_token="$(cat ./examples/jwt.txt)" + +curl -X POST http://localhost:3000/api/users/profile \ + -H "Authorization: Bearer ${my_token}" \ + -H 'Content-Type: application/json' \ + --data-binary '{ "foo": "bar" }' +``` + +## Example OIDC Discovery URLs + +- Demo: + +- Auth0: +- Okta: +- Google: diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..8ba7050 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,8 @@ +# Examples + +These example RSA and ECDSA private keys can be validated against the demo site: + +| Issuer | | +| :------------ | :-------------------------------------------------------------------------- | +| Discovery URL | | +| JWKs URL | | diff --git a/examples/generate-jwt.sh b/examples/generate-jwt.sh new file mode 100755 index 0000000..aca34ff --- /dev/null +++ b/examples/generate-jwt.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +keypairs sign \ + --exp 87660h \ + ./examples/privkey.ec.jwk.json \ + '{ + "iss": "https://therootcompany.github.io/libauth/", + "sub": "1", + "email_verified": false, + "email": "jo@example.com" + }' \ + > ./examples/jwt.txt \ + 2> ./examples/jws.json diff --git a/examples/go.mod b/examples/go.mod new file mode 100644 index 0000000..dc52405 --- /dev/null +++ b/examples/go.mod @@ -0,0 +1,9 @@ +module git.rootprojects.org/root/libauth/examples + +go 1.18 + +require ( + git.rootprojects.org/root/keypairs v0.6.5 + git.rootprojects.org/root/libauth v0.1.0 + github.com/go-chi/chi/v5 v5.0.7 +) diff --git a/examples/go.sum b/examples/go.sum new file mode 100644 index 0000000..29f4815 --- /dev/null +++ b/examples/go.sum @@ -0,0 +1,6 @@ +git.rootprojects.org/root/keypairs v0.6.5 h1:sdRAQD/O/JBS8+ZxUewXnY+cjQVDNH3TmcS+KtANZqA= +git.rootprojects.org/root/keypairs v0.6.5/go.mod h1:WGI8PadOp+4LjUuI+wNlSwcJwFtY8L9XuNjuO3213HA= +git.rootprojects.org/root/libauth v0.1.0 h1:qM73YYBLByoFTJUXH2ZeUhJdLzY35t4jgNoUAyqH2QA= +git.rootprojects.org/root/libauth v0.1.0/go.mod h1:bbLDWn0w7I1VfOMP2DZU/t/H9Ln0mT61K+ELH4ievVM= +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= diff --git a/examples/jws.json b/examples/jws.json new file mode 100644 index 0000000..c020735 --- /dev/null +++ b/examples/jws.json @@ -0,0 +1,18 @@ +{ + "claims": { + "email": "jo@example.com", + "email_verified": false, + "exp": 1967702403, + "iss": "https://therootcompany.github.io/libauth/", + "sub": "1" + }, + "header": { + "alg": "ES256", + "kid": "tsb1m6h3xjp1HjXmJY_8pTtHxld5UvWxoPG9b2e_0aY", + "typ": "JWT" + }, + "payload": "eyJlbWFpbCI6ImpvQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJleHAiOjE5Njc3MDI0MDMsImlzcyI6Imh0dHBzOi8vdGhlcm9vdGNvbXBhbnkuZ2l0aHViLmlvL2xpYmF1dGgvIiwic3ViIjoiMSJ9", + "protected": "eyJhbGciOiJFUzI1NiIsImtpZCI6InRzYjFtNmgzeGpwMUhqWG1KWV84cFR0SHhsZDVVdld4b1BHOWIyZV8wYVkiLCJ0eXAiOiJKV1QifQ", + "signature": "-vezm5OL5c4vlFvvj0Z4HAbX2nAAabO_37w5wMtnD2_OuzTDhM_4wRxzEZ5sdUIJ0rM7gGAv7B3CfGSibr0TJA" +} + diff --git a/examples/jwt.txt b/examples/jwt.txt new file mode 100644 index 0000000..2f7c76d --- /dev/null +++ b/examples/jwt.txt @@ -0,0 +1 @@ +eyJhbGciOiJFUzI1NiIsImtpZCI6InRzYjFtNmgzeGpwMUhqWG1KWV84cFR0SHhsZDVVdld4b1BHOWIyZV8wYVkiLCJ0eXAiOiJKV1QifQ.eyJlbWFpbCI6ImpvQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJleHAiOjE5Njc3MDI0MDMsImlzcyI6Imh0dHBzOi8vdGhlcm9vdGNvbXBhbnkuZ2l0aHViLmlvL2xpYmF1dGgvIiwic3ViIjoiMSJ9.-vezm5OL5c4vlFvvj0Z4HAbX2nAAabO_37w5wMtnD2_OuzTDhM_4wRxzEZ5sdUIJ0rM7gGAv7B3CfGSibr0TJA diff --git a/examples/privkey.ec.jwk.json b/examples/privkey.ec.jwk.json new file mode 100644 index 0000000..741f5ed --- /dev/null +++ b/examples/privkey.ec.jwk.json @@ -0,0 +1,7 @@ +{ + "crv": "P-256", + "d": "xcwk5FI9QuCK7Ap-aRsjdaHQx6ckoXcgQQ_HQbarUWE", + "kty": "EC", + "x": "VSQ5P-nwzOMVYowySPF8-FFRTLGXfY611ErayGgM4cc", + "y": "qSVv69YEJdM7waa_w8wegsaFDZaFZNkRNV-2PUGXb_E" +} diff --git a/examples/privkey.rsa.jwk.json b/examples/privkey.rsa.jwk.json new file mode 100644 index 0000000..5d91792 --- /dev/null +++ b/examples/privkey.rsa.jwk.json @@ -0,0 +1,11 @@ +{ + "d": "d3iFUdcRcBhR8mlG0jOQ_mClfkaMwquVTVqH3JdBf6CIiM21R1a2Rwzuyctjn9YIDlJGuHHF7ZHBL9LaHh13-QvcFgymgQV8qFFk3Fx813EZ6UeWsk7eT2lfbNW3pFXyXPnOvNsTWDIogISTUl0GsOkHbgjGvn4yIDJ033y_nVP6MybnTS7Z-bNZj7YUo9srlTUxXLHd8U9r8b5UIWRCyWeOhsxNjiCCOEinmL6dISjKeubpoG6mbBxC7ptglINqVYHu9HxFfOqd-epbQw0Y5DGIvd6iyzbsfmx8DJwXoJ31oyiFMPdgO5kbZbwEDBCxnHBzxH8M3kfYmL5uYRfnYQ", + "dp": "YAJfR37hBBwaXct4qkY7_pM2hHRtUOwEAq6QiSB24ogWzq8ehPfIAE_Vu1NAPhMVZk_UPt9A8444pUDLGXpe1-Y66TSC9l6x0g4LhX44A0sc6Wh5Cpbjjr77aim5aVf1AyR-SiPkHNY4SGl_VbkBtk6JOTlww5QwDEdbvcl8ugs", + "dq": "qO8KbV5svwEAdbHiKt0iFts53PQiD1p-XkvAeV4TPJKYfEmkQPqnpfe6pjN-dnl0S_OwdajLvnU_qVXs0Gec8hJHBf6nBuU0DpIxUML1R-aBqnEPH6-tH8pYK-fD0qr20Qr8tkR8hKI5cthOS0wiqh9A7FC4AUZxLgWOFhs0VdE", + "e": "AQAB", + "kty": "RSA", + "n": "q0yq8t-8Sw9nAJQAbDhiUMtxD_OEHigOekZrcLR38JkagqUZlxYZNp1B7NXM8GTymtz3qKzxUoI-mmE9gHq2nyDN8Jc_DTe_jnNFPD_bAxo92Ii_jpT74_6PR7I92BBvw0-ecxKHScJlO2tD2l1hxyOwpJ52Gt3WuXp2Ezsd3_14boTU4Z3Wh7WFNStz-BBwl09KR8UmVz1_pifJMnDEDXsRMEorFEbSDlJoZLAQgjAEwEZdmecH256WANKGylk1m5PWIBA59FMNXdQZIN1e6Cc0knaqZJHLor1hzmfSjyxxhSck0xk0HccUFNskS9QMoX05IvupxcnMBVPXIQBstw", + "p": "wzVny5EpwoVF_ljD7mxVVk0gjWTfJ2lkXunb5HbpB_1XkY464kptA9WzSa_0kuSegUrcAPL3KrLPx2ZOT9y9Q5q2RLH4MSCm2uarRvYTiKt9LkIdPFH68iUrrt8wAMX4KXA13mwD-hPpdxNB_Dz1qWqaodW_X-zXLEntBv2tUXs", + "q": "4KUoRNx164BsU8mFRV9fs4ZUm2fR-Z_JxlUj04dOcu4YYxQNmiSSXDUcTxx3-s4gphh1EVBv52eQ9R7tmmu9EEPIMWWbgdG5FclawDbiUuFnH7MsPT71y6NKWoxUfnzBivLgsdymruYUmJltXvH27pPeZoZetuAzzQFfM25ctvU", + "qi": "GbOKVpISiuLNPYzTSfQv0pYDDKwtdxyRlSPwU-0rjuYdzsTI6p4QRIbb-8pdWDTuBhmOuB3NUGGPRmeLn8187Z1iTsRSy7bjWmwqcXfOtc22En7OfTUhaUn-p83u9-NpFyGIkB8smZ-kV_g3DEwxovWvvcF3bAajvQCNrbqsOLQ" +} diff --git a/examples/server.go b/examples/server.go new file mode 100644 index 0000000..abae3e8 --- /dev/null +++ b/examples/server.go @@ -0,0 +1,86 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + + "github.com/go-chi/chi/v5" + + "git.rootprojects.org/root/keypairs/keyfetch" + "git.rootprojects.org/root/libauth" + "git.rootprojects.org/root/libauth/chiauth" +) + +func main() { + r := chi.NewRouter() + + whitelist, err := keyfetch.NewWhitelist([]string{"https://therootcompany.github.io/libauth/"}) + if nil != err { + panic(err) + } + + // Unauthenticated Routes + r.Group(func(r chi.Router) { + r.Post("/api/hello", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{ "message": "Hello, World!" }`)) + }) + }) + + // Authenticated Routes + r.Group(func(r chi.Router) { + tokenVerifier := chiauth.NewTokenVerifier(chiauth.VerificationParams{ + Issuers: whitelist, + Optional: true, + }) + r.Use(tokenVerifier) + + r.Post("/api/users/profile", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + jws, ok := ctx.Value(chiauth.JWSKey).(*libauth.JWS) + if !ok || !jws.Trusted { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + userID := jws.Claims["sub"].(string) + + b, _ := json.MarshalIndent(struct { + UserID string `json:"user_id"` + }{ + UserID: userID, + }, "", " ") + w.Write(append(b, '\n')) + }) + }) + + // ... + + bindAddr := ":3000" + fmt.Println("Listening on", bindAddr) + + fmt.Println("") + fmt.Println("Try this:") + fmt.Println("") + fmt.Println("") + cwd, _ := os.Getwd() + fmt.Println(" pushd", cwd) + fmt.Println("") + fmt.Println(" my_jwt=\"$(cat ./jwt.txt)\"") + fmt.Println( + strings.Join( + []string{ + " curl -X POST http://localhost:3000/api/users/profile", + " -H \"Authorization: Bearer ${my_jwt}\"", + " -H 'Content-Type: application/json'", + " --data-binary '{ \"foo\": \"bar\" }'", + }, + " \\\n", + ), + ) + fmt.Println("") + + http.ListenAndServe(bindAddr, r) +}