initial commit
This commit is contained in:
parent
dc8c1b743d
commit
9cdb2aaeee
62
README.md
62
README.md
|
@ -1,2 +1,62 @@
|
||||||
# libauth
|
# 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" }'
|
||||||
|
```
|
||||||
|
|
|
@ -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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue