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" ]
2022-05-09 19:34:08 +00:00
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
}