forked from TrueCloudLab/certificates
147 lines
3.8 KiB
Go
147 lines
3.8 KiB
Go
package provisioner
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/smallstep/cli/jose"
|
|
)
|
|
|
|
type openIDConfiguration struct {
|
|
Issuer string `json:"issuer"`
|
|
JWKSetURI string `json:"jwks_uri"`
|
|
}
|
|
|
|
// openIDPayload represents the fields on the id_token JWT payload.
|
|
type openIDPayload struct {
|
|
jose.Claims
|
|
AtHash string `json:"at_hash"`
|
|
AuthorizedParty string `json:"azp"`
|
|
Email string `json:"email"`
|
|
EmailVerified string `json:"email_verified"`
|
|
Hd string `json:"hd"`
|
|
Nonce string `json:"nonce"`
|
|
}
|
|
|
|
// OIDC represents an OAuth 2.0 OpenID Connect provider.
|
|
type OIDC struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
ClientID string `json:"clientID"`
|
|
ConfigurationEndpoint string `json:"configurationEndpoint"`
|
|
Claims *Claims `json:"claims,omitempty"`
|
|
Admins []string `json:"admins"`
|
|
configuration openIDConfiguration
|
|
keyStore *keyStore
|
|
}
|
|
|
|
// IsAdmin returns true if the given email is in the Admins whitelist, false
|
|
// otherwise.
|
|
func (o *OIDC) IsAdmin(email string) bool {
|
|
for _, e := range o.Admins {
|
|
if e == email {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Validate validates and initializes the OIDC provider.
|
|
func (o *OIDC) Validate() error {
|
|
switch {
|
|
case o.Name == "":
|
|
return errors.New("name cannot be empty")
|
|
case o.ClientID == "":
|
|
return errors.New("clientID cannot be empty")
|
|
case o.ConfigurationEndpoint == "":
|
|
return errors.New("configurationEndpoint cannot be empty")
|
|
}
|
|
|
|
// Decode openid-configuration endpoint
|
|
var conf openIDConfiguration
|
|
if err := getAndDecode(o.ConfigurationEndpoint, &conf); err != nil {
|
|
return err
|
|
}
|
|
if conf.JWKSetURI == "" {
|
|
return errors.Errorf("error parsing %s: jwks_uri cannot be empty", o.ConfigurationEndpoint)
|
|
}
|
|
// Get JWK key set
|
|
keyStore, err := newKeyStore(conf.JWKSetURI)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.configuration = conf
|
|
o.keyStore = keyStore
|
|
return nil
|
|
}
|
|
|
|
// ValidatePayload validates the given token payload.
|
|
//
|
|
// TODO(mariano): avoid reply attacks validating nonce.
|
|
func (o *OIDC) ValidatePayload(p openIDPayload) error {
|
|
// According to "rfc7519 JSON Web Token" acceptable skew should be no more
|
|
// than a few minutes.
|
|
if err := p.ValidateWithLeeway(jose.Expected{
|
|
Issuer: o.configuration.Issuer,
|
|
Audience: jose.Audience{o.ClientID},
|
|
}, time.Minute); err != nil {
|
|
return errors.Wrap(err, "failed to validate payload")
|
|
}
|
|
if p.AuthorizedParty != "" && p.AuthorizedParty != o.ClientID {
|
|
return errors.New("failed to validate payload: invalid azp")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Authorize validates the given token.
|
|
func (o *OIDC) Authorize(token string) ([]SignOption, error) {
|
|
jwt, err := jose.ParseSigned(token)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error parsing token")
|
|
}
|
|
|
|
var claims openIDPayload
|
|
// Parse claims to get the kid
|
|
if err := jwt.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
|
return nil, errors.Wrap(err, "error parsing claims")
|
|
}
|
|
|
|
found := false
|
|
kid := jwt.Headers[0].KeyID
|
|
keys := o.keyStore.Get(kid)
|
|
for _, key := range keys {
|
|
if err := jwt.Claims(key, &claims); err == nil {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return nil, errors.New("cannot validate token")
|
|
}
|
|
|
|
if err := o.ValidatePayload(claims); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if o.IsAdmin(claims.Email) {
|
|
return []SignOption{}, nil
|
|
}
|
|
|
|
return []SignOption{
|
|
emailOnlyIdentity(claims.Email),
|
|
}, nil
|
|
}
|
|
|
|
func getAndDecode(uri string, v interface{}) error {
|
|
resp, err := http.Get(uri)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to connect to %s", uri)
|
|
}
|
|
defer resp.Body.Close()
|
|
if err := json.NewDecoder(resp.Body).Decode(v); err != nil {
|
|
return errors.Wrapf(err, "error reading %s", uri)
|
|
}
|
|
return nil
|
|
}
|