You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
smallstep-certificates/authority/provisioner/oidc.go

148 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
}