From bdad6bf8edaf0d04bbb3bcc480d862d9c548899e Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 9 May 2022 17:08:38 -0600 Subject: [PATCH] feat: add more convenience methods --- examples/go.mod | 7 ++++- examples/go.sum | 4 +++ examples/server.go | 27 ++++++++++++---- libauth.go | 77 ++++++++++++++++++++++++++++++++++++---------- 4 files changed, 92 insertions(+), 23 deletions(-) diff --git a/examples/go.mod b/examples/go.mod index dc52405..a765630 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -3,7 +3,12 @@ module git.rootprojects.org/root/libauth/examples go 1.18 require ( - git.rootprojects.org/root/keypairs v0.6.5 + 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 +) diff --git a/examples/go.sum b/examples/go.sum index 29f4815..c8b94cb 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -1,6 +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= diff --git a/examples/server.go b/examples/server.go index 3b8c501..5011077 100644 --- a/examples/server.go +++ b/examples/server.go @@ -7,24 +7,39 @@ import ( "os" "strings" - "github.com/go-chi/chi/v5" - - "git.rootprojects.org/root/keypairs/keyfetch" + "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() - whitelist, err := keyfetch.NewWhitelist([]string{"https://therootcompany.github.io/libauth/"}) + 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) { - w.Write([]byte(`{ "message": "Hello, World!" }`)) + jws := chiauth.GetJWS(r) + + w.Write([]byte( + fmt.Sprintf(`{ "message": "Hello, World!", "authenticated": %t }`, jws.Trusted), + )) }) }) @@ -32,7 +47,7 @@ func main() { r.Group(func(r chi.Router) { tokenVerifier := chiauth.NewTokenVerifier(chiauth.VerificationParams{ Issuers: whitelist, - Optional: true, + Optional: false, }) r.Use(tokenVerifier) diff --git a/libauth.go b/libauth.go index b9ea0e9..cdfdcae 100644 --- a/libauth.go +++ b/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,26 +21,69 @@ 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") } - 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) @@ -67,18 +114,16 @@ func VerifyJWS(jws *keypairs.JWS, issuers keyfetch.Whitelist, r *http.Request) ( return nil, fmt.Errorf("Bad Request: self-signed tokens with 'jwk' are not supported") } - errs := keypairs.VerifyClaims(pub, jws) + errs := keypairs.VerifyClaims(pub, &jws.JWS) if 0 != len(errs) { 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%s", strings.Join(strs, "\n\t")) } - return &JWS{ - JWS: *jws, - Trusted: true, - Errors: nil, - }, nil + jws.Trusted = true + return jws, nil }