initial commit

This commit is contained in:
AJ ONeal 2022-05-05 18:11:08 -06:00
parent dc8c1b743d
commit 9cdb2aaeee
No known key found for this signature in database
GPG Key ID: 562702827EF68D87
3 changed files with 237 additions and 1 deletions

View File

@ -1,2 +1,62 @@
# libauth
LibAuth for Go - The 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]
[godoc]: https://pkg.go.dev/git.rootprojects.org/root/libauth?tab=versions
[godoc_button]: https://godoc.org/git.rootprojects.org/root/libauth?status.svg
## Example Usage
How to verify a valid, trusted token as `chi` middleware:
```go
package main
import (
"net/http"
"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://accounts.google.com"})
if nil != err {
panic(err)
}
tokenVerifier := chiauth.NewTokenVerifier(chiauth.VerificationParams{
Issuers: whitelist,
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)
if !ok || !jws.Trusted {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
userID := jws.Claims["sub"].(string)
// ...
})
// ...
}
```
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" }'
```

92
chiauth/chiauth.go Normal file
View File

@ -0,0 +1,92 @@
package chiauth
import (
"context"
"log"
"net/http"
"strings"
"git.rootprojects.org/root/keypairs/keyfetch"
"git.rootprojects.org/root/libauth"
)
type ctxKey string
// JWSKey is used to get the InspectableToken from http.Request.Context().Value(chiauth.JWSKey)
var JWSKey = ctxKey("jws")
// VerificationParams specify the Issuer and whether or not the token is Optional (if provided, it must pass verification)
type VerificationParams struct {
Issuers keyfetch.Whitelist
Optional bool
/*
ExpLeeway int
NbfLeeway int
SelfSigned bool
PubKey keypairs.PublicKey
*/
}
// 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)
// })
//
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 opts.Optional {
next.ServeHTTP(w, r)
return
}
http.Error(
w,
"Bad Format: missing Authorization header and 'access_token' query",
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,
)
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))
return
}
if !inspected.Trusted {
http.Error(w, "Bad Token Signature", http.StatusBadRequest)
return
}
ctx := context.WithValue(r.Context(), JWSKey, inspected)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

84
libauth.go Normal file
View File

@ -0,0 +1,84 @@
package libauth
import (
"fmt"
"net/http"
"strings"
"git.rootprojects.org/root/keypairs"
"git.rootprojects.org/root/keypairs/keyfetch"
)
// JWS is keypairs.JWS with added debugging information
type JWS struct {
keypairs.JWS
Trusted bool `json:"trusted"`
Errors []error `json:"errors,omitempty"`
}
// 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) {
jws := keypairs.JWTToJWS(jwt)
if nil == jws {
return nil, fmt.Errorf("Bad Request: malformed Authorization header")
}
if err := jws.DecodeComponents(); nil != err {
return &JWS{
*jws,
false,
[]error{err},
}, err
}
return VerifyJWS(jws, 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) {
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) {
//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) {
//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")
} 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")
}
}
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")
}
} else {
return nil, fmt.Errorf("Bad Request: self-signed tokens with 'jwk' are not supported")
}
errs := keypairs.VerifyClaims(pub, jws)
if 0 != len(errs) {
strs := []string{}
for _, err := range errs {
strs = append(strs, err.Error())
}
return nil, fmt.Errorf("invalid jwt:\n%s", strings.Join(strs, "\n\t"))
}
return &JWS{
JWS: *jws,
Trusted: true,
Errors: nil,
}, nil
}