Compare commits

...

12 Commits
v0.1.0 ... main

Author SHA1 Message Date
1dece66bee
feat: update error messages 2023-04-04 17:22:57 -06:00
0c2f482c9e
chore(lint): nix yoda conditions 2023-04-04 17:12:52 -06:00
0f2f6734b0
chore(lint): lowercase error messages 2023-04-04 17:03:59 -06:00
d832ea7304
chore: go fmt 2023-04-04 17:00:04 -06:00
AJ ONeal
bdad6bf8ed
feat: add more convenience methods 2022-05-09 17:08:38 -06:00
AJ ONeal
97d0f2eec0
feat(chiauth): add GetJWS(r) helper 2022-05-09 15:14:12 -06:00
AJ ONeal
7c8ea53a6c
security: remove debug token logging 2022-05-09 15:13:05 -06:00
AJ ONeal
f5e3b9e094
build: include .well-known in GitHub Pages 2022-05-09 14:00:22 -06:00
AJ ONeal
ba52e17003
chore: add Prettier config (for Markdown, JSON) 2022-05-09 14:00:22 -06:00
AJ ONeal
70b0af6796
feat(example): add example server 2022-05-09 14:00:21 -06:00
AJ ONeal
c529b415bc
nit(chiauth): add trailing newline to error response 2022-05-09 13:34:30 -06:00
AJ ONeal
c3402609b4
bugfix(claims): invert jwk check condition 2022-05-09 13:34:08 -06:00
17 changed files with 354 additions and 63 deletions

6
.prettierrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"printWidth": 80,
"tabWidth": 4,
"singleQuote": false,
"proseWrap": "always"
}

19
.well-known/jwks.json Normal file
View 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"
}
]
}

View File

@ -0,0 +1,4 @@
{
"issuer": "https://therootcompany.github.io/libauth/",
"jwks_uri": "https://therootcompany.github.io/libauth/.well-known/jwks.json"
}

View File

@ -0,0 +1 @@
openid-configuration

View File

@ -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
View File

@ -0,0 +1 @@
include: [".well-known"]

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
eyJhbGciOiJFUzI1NiIsImtpZCI6InRzYjFtNmgzeGpwMUhqWG1KWV84cFR0SHhsZDVVdld4b1BHOWIyZV8wYVkiLCJ0eXAiOiJKV1QifQ.eyJlbWFpbCI6ImpvQGV4YW1wbGUuY29tIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJleHAiOjE5Njc3MDI0MDMsImlzcyI6Imh0dHBzOi8vdGhlcm9vdGNvbXBhbnkuZ2l0aHViLmlvL2xpYmF1dGgvIiwic3ViIjoiMSJ9.-vezm5OL5c4vlFvvj0Z4HAbX2nAAabO_37w5wMtnD2_OuzTDhM_4wRxzEZ5sdUIJ0rM7gGAv7B3CfGSibr0TJA

View File

@ -0,0 +1,7 @@
{
"crv": "P-256",
"d": "xcwk5FI9QuCK7Ap-aRsjdaHQx6ckoXcgQQ_HQbarUWE",
"kty": "EC",
"x": "VSQ5P-nwzOMVYowySPF8-FFRTLGXfY611ErayGgM4cc",
"y": "qSVv69YEJdM7waa_w8wegsaFDZaFZNkRNV-2PUGXb_E"
}

View 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
View 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)
}

View File

@ -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
}