initial commit
This commit is contained in:
parent
dc8c1b743d
commit
9cdb2aaeee
62
README.md
62
README.md
|
@ -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" }'
|
||||
```
|
||||
|
|
|
@ -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