mirror of
https://github.com/therootcompany/libauth.git
synced 2025-12-23 22:08:47 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1dece66bee | |||
| 0c2f482c9e | |||
| 0f2f6734b0 | |||
| d832ea7304 | |||
|
|
bdad6bf8ed | ||
|
|
97d0f2eec0 | ||
|
|
7c8ea53a6c | ||
|
|
f5e3b9e094 | ||
|
|
ba52e17003 | ||
|
|
70b0af6796 | ||
|
|
c529b415bc | ||
|
|
c3402609b4 |
6
.prettierrc.json
Normal file
6
.prettierrc.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": false,
|
||||
"proseWrap": "always"
|
||||
}
|
||||
19
.well-known/jwks.json
Normal file
19
.well-known/jwks.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
4
.well-known/openid-configuration
Normal file
4
.well-known/openid-configuration
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"issuer": "https://therootcompany.github.io/libauth/",
|
||||
"jwks_uri": "https://therootcompany.github.io/libauth/.well-known/jwks.json"
|
||||
}
|
||||
1
.well-known/openid-configuration.json
Symbolic link
1
.well-known/openid-configuration.json
Symbolic link
@ -0,0 +1 @@
|
||||
openid-configuration
|
||||
52
README.md
52
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)
|
||||
}
|
||||
@ -38,9 +39,8 @@ func main() {
|
||||
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 {
|
||||
jws := chiauth.GetJWS(r)
|
||||
if nil == jws || !jws.Trusted {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
@ -53,11 +53,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 <xxxx.yyyy.zzzz>' \
|
||||
-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:
|
||||
<https://therootcompany.github.io/libauth/.well-known/openid-configuration>
|
||||
- Auth0: <https://example.auth0.com/.well-known/openid-configuration>
|
||||
- Okta: <https://example.okta.com/.well-known/openid-configuration>
|
||||
- Google: <https://accounts.google.com/.well-known/openid-configuration>
|
||||
|
||||
1
_config.yaml
Normal file
1
_config.yaml
Normal file
@ -0,0 +1 @@
|
||||
include: [".well-known"]
|
||||
@ -2,7 +2,6 @@ package chiauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@ -29,59 +28,51 @@ type VerificationParams struct {
|
||||
|
||||
// NewTokenVerifier returns a token-verifying middleware
|
||||
//
|
||||
// tokenVerifier := chiauth.NewTokenVerifier(chiauth.VerificationParams{
|
||||
// Issuers: keyfetch.Whitelist([]string{"https://accounts.google.com"}),
|
||||
// Optional: false,
|
||||
// })
|
||||
// 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)
|
||||
// })
|
||||
// tokenVerifier := chiauth.NewTokenVerifier(chiauth.VerificationParams{
|
||||
// Issuers: keyfetch.Whitelist([]string{"https://accounts.google.com"}),
|
||||
// Optional: false,
|
||||
// })
|
||||
// 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)
|
||||
// })
|
||||
func NewTokenVerifier(opts VerificationParams) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// just setting a default, other handlers can change this
|
||||
|
||||
token := r.Header.Get("Authorization")
|
||||
log.Printf("%s %s %s\n", r.Method, r.URL.Path, token)
|
||||
|
||||
if "" == token {
|
||||
if token == "" {
|
||||
if opts.Optional {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(
|
||||
w,
|
||||
"Bad Format: missing Authorization header and 'access_token' query",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
errmsg := "bad format: missing 'Authorization' header and 'access_token' query"
|
||||
http.Error(w, errmsg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(token, " ")
|
||||
if 2 != len(parts) {
|
||||
http.Error(
|
||||
w,
|
||||
"Bad Format: expected Authorization header to be in the format of 'Bearer <Token>'",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
if len(parts) != 2 {
|
||||
errmsg := "bad format: expected 'Authorization' header to be in the format of 'Bearer <Token>'"
|
||||
http.Error(w, errmsg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
token = parts[1]
|
||||
|
||||
inspected, err := libauth.VerifyJWT(token, opts.Issuers, r)
|
||||
if nil != err {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
errmsg := "Invalid Token: " + err.Error()
|
||||
w.Write([]byte(errmsg))
|
||||
errmsg := "invalid token: " + err.Error()
|
||||
http.Error(w, errmsg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !inspected.Trusted {
|
||||
http.Error(w, "Bad Token Signature", http.StatusBadRequest)
|
||||
errmsg := "invalid token: bad signature"
|
||||
http.Error(w, errmsg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
@ -90,3 +81,10 @@ func NewTokenVerifier(opts VerificationParams) func(http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetJWS retrieves *libauth.JWS from r.Context()
|
||||
func GetJWS(r *http.Request) *libauth.JWS {
|
||||
ctx := r.Context()
|
||||
jws, _ := ctx.Value(JWSKey).(*libauth.JWS)
|
||||
return jws
|
||||
}
|
||||
|
||||
8
examples/README.md
Normal file
8
examples/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Examples
|
||||
|
||||
These example RSA and ECDSA private keys can be validated against the demo site:
|
||||
|
||||
| Issuer | <https://therootcompany.github.io/libauth/> |
|
||||
| :------------ | :-------------------------------------------------------------------------- |
|
||||
| Discovery URL | <https://therootcompany.github.io/libauth/.well-known/openid-configuration> |
|
||||
| JWKs URL | <https://therootcompany.github.io/libauth/.well-known/jwks.json> |
|
||||
13
examples/generate-jwt.sh
Executable file
13
examples/generate-jwt.sh
Executable file
@ -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
|
||||
14
examples/go.mod
Normal file
14
examples/go.mod
Normal file
@ -0,0 +1,14 @@
|
||||
module git.rootprojects.org/root/libauth/examples
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
git.rootprojects.org/root/dotenv v1.0.0
|
||||
git.rootprojects.org/root/libauth v0.1.0
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
)
|
||||
|
||||
require (
|
||||
git.rootprojects.org/root/keypairs v0.6.5 // indirect
|
||||
github.com/joho/godotenv v1.3.0 // indirect
|
||||
)
|
||||
10
examples/go.sum
Normal file
10
examples/go.sum
Normal file
@ -0,0 +1,10 @@
|
||||
git.rootprojects.org/root/dotenv v1.0.0 h1:aQIOAghd9DlgicIH+Z0K9HPjGFnMdtRAEoOmnT3A0uw=
|
||||
git.rootprojects.org/root/dotenv v1.0.0/go.mod h1:sA3rt078/Uc8/roUvD4DQZk/BtmrYPGv3o4DTqwnfa0=
|
||||
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=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
18
examples/jws.json
Normal file
18
examples/jws.json
Normal file
@ -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"
|
||||
}
|
||||
|
||||
1
examples/jwt.txt
Normal file
1
examples/jwt.txt
Normal file
@ -0,0 +1 @@
|
||||
eyJhbGciOiJFUzI1NiIsImtpZCI6InRzYjFtNmgzeGpwMUhqWG1KWV84cFR0SHhsZDVVdld4b1BHOWIyZV8wYVkiLCJ0eXAiOiJKV1QifQ.eyJlbWFpbCI6ImpvQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJleHAiOjE5Njc3MDI0MDMsImlzcyI6Imh0dHBzOi8vdGhlcm9vdGNvbXBhbnkuZ2l0aHViLmlvL2xpYmF1dGgvIiwic3ViIjoiMSJ9.-vezm5OL5c4vlFvvj0Z4HAbX2nAAabO_37w5wMtnD2_OuzTDhM_4wRxzEZ5sdUIJ0rM7gGAv7B3CfGSibr0TJA
|
||||
7
examples/privkey.ec.jwk.json
Normal file
7
examples/privkey.ec.jwk.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"crv": "P-256",
|
||||
"d": "xcwk5FI9QuCK7Ap-aRsjdaHQx6ckoXcgQQ_HQbarUWE",
|
||||
"kty": "EC",
|
||||
"x": "VSQ5P-nwzOMVYowySPF8-FFRTLGXfY611ErayGgM4cc",
|
||||
"y": "qSVv69YEJdM7waa_w8wegsaFDZaFZNkRNV-2PUGXb_E"
|
||||
}
|
||||
11
examples/privkey.rsa.jwk.json
Normal file
11
examples/privkey.rsa.jwk.json
Normal file
@ -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"
|
||||
}
|
||||
99
examples/server.go
Normal file
99
examples/server.go
Normal file
@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.rootprojects.org/root/libauth"
|
||||
"git.rootprojects.org/root/libauth/chiauth"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
godotenv.Load(".env")
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
if 0 == len(os.Getenv("OIDC_ISSUERS")) {
|
||||
os.Setenv("OIDC_ISSUERS", "https://therootcompany.github.io/libauth/")
|
||||
}
|
||||
whitelist, err := libauth.ParseIssuerEnvs("OIDC_ISSUERS", "OIDC_ISSUERS_INTERNAL")
|
||||
if nil != err {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Unauthenticated Routes
|
||||
r.Group(func(r chi.Router) {
|
||||
tokenVerifier := chiauth.NewTokenVerifier(chiauth.VerificationParams{
|
||||
Issuers: whitelist,
|
||||
Optional: true,
|
||||
})
|
||||
r.Use(tokenVerifier)
|
||||
r.Post("/api/hello", func(w http.ResponseWriter, r *http.Request) {
|
||||
jws := chiauth.GetJWS(r)
|
||||
|
||||
w.Write([]byte(
|
||||
fmt.Sprintf(`{ "message": "Hello, World!", "authenticated": %t }`, jws.Trusted),
|
||||
))
|
||||
})
|
||||
})
|
||||
|
||||
// Authenticated Routes
|
||||
r.Group(func(r chi.Router) {
|
||||
tokenVerifier := chiauth.NewTokenVerifier(chiauth.VerificationParams{
|
||||
Issuers: whitelist,
|
||||
Optional: false,
|
||||
})
|
||||
r.Use(tokenVerifier)
|
||||
|
||||
r.Post("/api/users/profile", func(w http.ResponseWriter, r *http.Request) {
|
||||
jws := chiauth.GetJWS(r)
|
||||
if nil == jws || !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)
|
||||
}
|
||||
99
libauth.go
99
libauth.go
@ -3,12 +3,16 @@ package libauth
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.rootprojects.org/root/keypairs"
|
||||
"git.rootprojects.org/root/keypairs/keyfetch"
|
||||
)
|
||||
|
||||
const oidcIssuersEnv = "OIDC_ISSUERS"
|
||||
const oidcIssuersInternalEnv = "OIDC_ISSUERS_INTERNAL"
|
||||
|
||||
// JWS is keypairs.JWS with added debugging information
|
||||
type JWS struct {
|
||||
keypairs.JWS
|
||||
@ -17,68 +21,111 @@ type JWS struct {
|
||||
Errors []error `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// IssuerList is the trusted list of token issuers
|
||||
type IssuerList = keyfetch.Whitelist
|
||||
|
||||
// ParseIssuerEnvs will parse ENVs (both comma- and space-delimited) to
|
||||
// create a trusted IssuerList of public and/or internal issuer URLs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// OIDC_ISSUERS='https://example.com/ https://therootcompany.github.io/libauth/'
|
||||
// OIDC_ISSUERS_INTERNAL='http://localhost:3000/ http://my-service-name:8080/'
|
||||
func ParseIssuerEnvs(issuersEnvName, internalEnvName string) (IssuerList, error) {
|
||||
if len(issuersEnvName) > 0 {
|
||||
issuersEnvName = oidcIssuersEnv
|
||||
}
|
||||
pubs := os.Getenv(issuersEnvName)
|
||||
pubURLs := ParseIssuerListString(pubs)
|
||||
|
||||
if len(internalEnvName) > 0 {
|
||||
internalEnvName = oidcIssuersInternalEnv
|
||||
}
|
||||
internals := os.Getenv(internalEnvName)
|
||||
internalURLs := ParseIssuerListString(internals)
|
||||
|
||||
return keyfetch.NewWhitelist(pubURLs, internalURLs)
|
||||
}
|
||||
|
||||
// ParseIssuerListString will Split comma- and/or space-delimited list into a slice
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// "https://example.com/, https://therootcompany.github.io/libauth/"
|
||||
func ParseIssuerListString(issuerList string) []string {
|
||||
issuers := []string{}
|
||||
|
||||
issuerList = strings.TrimSpace(issuerList)
|
||||
if len(issuerList) > 0 {
|
||||
issuerList = strings.ReplaceAll(issuerList, ",", " ")
|
||||
issuers = strings.Fields(issuerList)
|
||||
}
|
||||
|
||||
return issuers
|
||||
}
|
||||
|
||||
// VerifyJWT will return a verified InspectableToken if possible, or otherwise as much detail as possible, possibly including an InspectableToken with failed verification.
|
||||
func VerifyJWT(jwt string, issuers keyfetch.Whitelist, r *http.Request) (*JWS, error) {
|
||||
func VerifyJWT(jwt string, issuers IssuerList, r *http.Request) (*JWS, error) {
|
||||
jws := keypairs.JWTToJWS(jwt)
|
||||
if nil == jws {
|
||||
return nil, fmt.Errorf("Bad Request: malformed Authorization header")
|
||||
return nil, fmt.Errorf("bad request: bearer token could not be parsed from 'Authorization' header")
|
||||
}
|
||||
|
||||
if err := jws.DecodeComponents(); nil != err {
|
||||
return &JWS{
|
||||
*jws,
|
||||
false,
|
||||
[]error{err},
|
||||
}, err
|
||||
myJws := &JWS{
|
||||
*jws,
|
||||
false,
|
||||
[]error{},
|
||||
}
|
||||
if err := myJws.DecodeComponents(); nil != err {
|
||||
myJws.Errors = append(myJws.Errors, err)
|
||||
return myJws, err
|
||||
}
|
||||
|
||||
return VerifyJWS(jws, issuers, r)
|
||||
return VerifyJWS(myJws, issuers, r)
|
||||
}
|
||||
|
||||
// VerifyJWS takes a fully decoded JWS and will return a verified InspectableToken if possible, or otherwise as much detail as possible, possibly including an InspectableToken with failed verification.
|
||||
func VerifyJWS(jws *keypairs.JWS, issuers keyfetch.Whitelist, r *http.Request) (*JWS, error) {
|
||||
func VerifyJWS(jws *JWS, issuers IssuerList, r *http.Request) (*JWS, error) {
|
||||
var pub keypairs.PublicKey
|
||||
kid, kidOK := jws.Header["kid"].(string)
|
||||
iss, issOK := jws.Claims["iss"].(string)
|
||||
|
||||
_, jwkOK := jws.Header["jwk"]
|
||||
if jwkOK {
|
||||
if !kidOK || 0 == len(kid) {
|
||||
if !jwkOK {
|
||||
if !kidOK || len(kid) == 0 {
|
||||
//errs = append(errs, "must have either header.kid or header.jwk")
|
||||
return nil, fmt.Errorf("Bad Request: missing 'kid' identifier")
|
||||
} else if !issOK || 0 == len(iss) {
|
||||
return nil, fmt.Errorf("bad request: missing 'kid' identifier")
|
||||
} else if !issOK || len(iss) == 0 {
|
||||
//errs = append(errs, "payload.iss must exist to complement header.kid")
|
||||
return nil, fmt.Errorf("Bad Request: payload.iss must exist to complement header.kid")
|
||||
return nil, fmt.Errorf("bad request: 'payload.iss' must exist to complement 'header.kid'")
|
||||
} else {
|
||||
// TODO beware domain fronting, we should set domain statically
|
||||
// See https://pkg.go.dev/git.rootprojects.org/root/keypairs@v0.6.2/keyfetch
|
||||
// (Caddy does protect against Domain-Fronting by default:
|
||||
// https://github.com/caddyserver/caddy/issues/2500)
|
||||
if !issuers.IsTrustedIssuer(iss, r) {
|
||||
return nil, fmt.Errorf("Bad Request: 'iss' is not a trusted issuer")
|
||||
return nil, fmt.Errorf("unauthorized: 'iss' (%s) is not a trusted issuer", iss)
|
||||
}
|
||||
}
|
||||
var err error
|
||||
pub, err = keyfetch.OIDCJWK(kid, iss)
|
||||
if nil != err {
|
||||
return nil, fmt.Errorf("Bad Request: 'kid' could not be matched to a known public key")
|
||||
return nil, fmt.Errorf("bad request: 'kid' could not be matched to a known public key: %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("Bad Request: self-signed tokens with 'jwk' are not supported")
|
||||
return nil, fmt.Errorf("bad request: self-signed tokens with 'jwk' are not supported")
|
||||
}
|
||||
|
||||
errs := keypairs.VerifyClaims(pub, jws)
|
||||
if 0 != len(errs) {
|
||||
errs := keypairs.VerifyClaims(pub, &jws.JWS)
|
||||
if len(errs) != 0 {
|
||||
strs := []string{}
|
||||
for _, err := range errs {
|
||||
jws.Errors = append(jws.Errors, err)
|
||||
strs = append(strs, err.Error())
|
||||
}
|
||||
return nil, fmt.Errorf("invalid jwt:\n%s", strings.Join(strs, "\n\t"))
|
||||
return jws, fmt.Errorf("invalid jwt:\n\t%s", strings.Join(strs, "\n\t"))
|
||||
}
|
||||
|
||||
return &JWS{
|
||||
JWS: *jws,
|
||||
Trusted: true,
|
||||
Errors: nil,
|
||||
}, nil
|
||||
jws.Trusted = true
|
||||
return jws, nil
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user