libauth/libauth.go

132 lines
4.1 KiB
Go
Raw Permalink Normal View History

2022-05-06 00:11:08 +00:00
package libauth
import (
"fmt"
"net/http"
2022-05-09 23:08:38 +00:00
"os"
2022-05-06 00:11:08 +00:00
"strings"
"git.rootprojects.org/root/keypairs"
"git.rootprojects.org/root/keypairs/keyfetch"
)
2022-05-09 23:08:38 +00:00
const oidcIssuersEnv = "OIDC_ISSUERS"
const oidcIssuersInternalEnv = "OIDC_ISSUERS_INTERNAL"
2022-05-06 00:11:08 +00:00
// JWS is keypairs.JWS with added debugging information
type JWS struct {
keypairs.JWS
Trusted bool `json:"trusted"`
Errors []error `json:"errors,omitempty"`
}
2022-05-09 23:08:38 +00:00
// 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:
2023-04-04 23:00:04 +00:00
//
// OIDC_ISSUERS='https://example.com/ https://therootcompany.github.io/libauth/'
// OIDC_ISSUERS_INTERNAL='http://localhost:3000/ http://my-service-name:8080/'
2022-05-09 23:08:38 +00:00
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:
2023-04-04 23:00:04 +00:00
//
// "https://example.com/, https://therootcompany.github.io/libauth/"
2022-05-09 23:08:38 +00:00
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
}
2022-05-06 00:11:08 +00:00
// VerifyJWT will return a verified InspectableToken if possible, or otherwise as much detail as possible, possibly including an InspectableToken with failed verification.
2022-05-09 23:08:38 +00:00
func VerifyJWT(jwt string, issuers IssuerList, r *http.Request) (*JWS, error) {
2022-05-06 00:11:08 +00:00
jws := keypairs.JWTToJWS(jwt)
if nil == jws {
2023-04-04 23:22:57 +00:00
return nil, fmt.Errorf("bad request: bearer token could not be parsed from 'Authorization' header")
2022-05-06 00:11:08 +00:00
}
2022-05-09 23:08:38 +00:00
myJws := &JWS{
*jws,
false,
[]error{},
}
if err := myJws.DecodeComponents(); nil != err {
myJws.Errors = append(myJws.Errors, err)
return myJws, err
2022-05-06 00:11:08 +00:00
}
2022-05-09 23:08:38 +00:00
return VerifyJWS(myJws, issuers, r)
2022-05-06 00:11:08 +00:00
}
// 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.
2022-05-09 23:08:38 +00:00
func VerifyJWS(jws *JWS, issuers IssuerList, r *http.Request) (*JWS, error) {
2022-05-06 00:11:08 +00:00
var pub keypairs.PublicKey
kid, kidOK := jws.Header["kid"].(string)
iss, issOK := jws.Claims["iss"].(string)
_, jwkOK := jws.Header["jwk"]
if !jwkOK {
2023-04-04 23:12:52 +00:00
if !kidOK || len(kid) == 0 {
2022-05-06 00:11:08 +00:00
//errs = append(errs, "must have either header.kid or header.jwk")
2023-04-04 23:03:59 +00:00
return nil, fmt.Errorf("bad request: missing 'kid' identifier")
2023-04-04 23:12:52 +00:00
} else if !issOK || len(iss) == 0 {
2022-05-06 00:11:08 +00:00
//errs = append(errs, "payload.iss must exist to complement header.kid")
2023-04-04 23:22:57 +00:00
return nil, fmt.Errorf("bad request: 'payload.iss' must exist to complement 'header.kid'")
2022-05-06 00:11:08 +00:00
} 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) {
2023-04-04 23:22:57 +00:00
return nil, fmt.Errorf("unauthorized: 'iss' (%s) is not a trusted issuer", iss)
2022-05-06 00:11:08 +00:00
}
}
var err error
pub, err = keyfetch.OIDCJWK(kid, iss)
if nil != err {
2023-04-04 23:03:59 +00:00
return nil, fmt.Errorf("bad request: 'kid' could not be matched to a known public key: %w", err)
2022-05-06 00:11:08 +00:00
}
} else {
2023-04-04 23:03:59 +00:00
return nil, fmt.Errorf("bad request: self-signed tokens with 'jwk' are not supported")
2022-05-06 00:11:08 +00:00
}
2022-05-09 23:08:38 +00:00
errs := keypairs.VerifyClaims(pub, &jws.JWS)
2023-04-04 23:12:52 +00:00
if len(errs) != 0 {
2022-05-06 00:11:08 +00:00
strs := []string{}
for _, err := range errs {
2022-05-09 23:08:38 +00:00
jws.Errors = append(jws.Errors, err)
2022-05-06 00:11:08 +00:00
strs = append(strs, err.Error())
}
2023-04-04 23:22:57 +00:00
return jws, fmt.Errorf("invalid jwt:\n\t%s", strings.Join(strs, "\n\t"))
2022-05-06 00:11:08 +00:00
}
2022-05-09 23:08:38 +00:00
jws.Trusted = true
return jws, nil
2022-05-06 00:11:08 +00:00
}