initial commit
This commit is contained in:
commit
6d0890e280
68 changed files with 5986 additions and 0 deletions
363
pkg/rp/default_verifier.go
Normal file
363
pkg/rp/default_verifier.go
Normal file
|
@ -0,0 +1,363 @@
|
|||
package rp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
|
||||
"github.com/caos/oidc/pkg/oidc"
|
||||
"github.com/caos/oidc/pkg/utils"
|
||||
)
|
||||
|
||||
//DefaultVerifier implements the `Verifier` interface
|
||||
type DefaultVerifier struct {
|
||||
config *verifierConfig
|
||||
keySet oidc.KeySet
|
||||
}
|
||||
|
||||
//ConfFunc is the type for providing dynamic options to the DefaultVerfifier
|
||||
type ConfFunc func(*verifierConfig)
|
||||
|
||||
//ACRVerifier specifies the function to be used by the `DefaultVerifier` for validating the acr claim
|
||||
type ACRVerifier func(string) error
|
||||
|
||||
//NewDefaultVerifier creates `DefaultVerifier` with the given
|
||||
//issuer, clientID, keyset and possible configOptions
|
||||
func NewDefaultVerifier(issuer, clientID string, keySet oidc.KeySet, confOpts ...ConfFunc) Verifier {
|
||||
conf := &verifierConfig{
|
||||
issuer: issuer,
|
||||
clientID: clientID,
|
||||
iat: &iatConfig{
|
||||
// offset: time.Duration(500 * time.Millisecond),
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range confOpts {
|
||||
if opt != nil {
|
||||
opt(conf)
|
||||
}
|
||||
}
|
||||
return &DefaultVerifier{config: conf, keySet: keySet}
|
||||
}
|
||||
|
||||
//WithIgnoreIssuedAt will turn off iat claim verification
|
||||
func WithIgnoreIssuedAt() func(*verifierConfig) {
|
||||
return func(conf *verifierConfig) {
|
||||
conf.iat.ignore = true
|
||||
}
|
||||
}
|
||||
|
||||
//WithIssuedAtOffset mitigates the risk of iat to be in the future
|
||||
//because of clock skews with the ability to add an offset to the current time
|
||||
func WithIssuedAtOffset(offset time.Duration) func(*verifierConfig) {
|
||||
return func(conf *verifierConfig) {
|
||||
conf.iat.offset = offset
|
||||
}
|
||||
}
|
||||
|
||||
//WithIssuedAtMaxAge provides the ability to define the maximum duration between iat and now
|
||||
func WithIssuedAtMaxAge(maxAge time.Duration) func(*verifierConfig) {
|
||||
return func(conf *verifierConfig) {
|
||||
conf.iat.maxAge = maxAge
|
||||
}
|
||||
}
|
||||
|
||||
//WithNonce TODO: ?
|
||||
func WithNonce(nonce string) func(*verifierConfig) {
|
||||
return func(conf *verifierConfig) {
|
||||
conf.nonce = nonce
|
||||
}
|
||||
}
|
||||
|
||||
//WithACRVerifier sets the verifier for the acr claim
|
||||
func WithACRVerifier(verifier ACRVerifier) func(*verifierConfig) {
|
||||
return func(conf *verifierConfig) {
|
||||
conf.acr = verifier
|
||||
}
|
||||
}
|
||||
|
||||
//WithAuthTimeMaxAge provides the ability to define the maximum duration between auth_time and now
|
||||
func WithAuthTimeMaxAge(maxAge time.Duration) func(*verifierConfig) {
|
||||
return func(conf *verifierConfig) {
|
||||
conf.maxAge = maxAge
|
||||
}
|
||||
}
|
||||
|
||||
//WithSupportedSigningAlgorithms overwrites the default RS256 signing algorithm
|
||||
func WithSupportedSigningAlgorithms(algs ...string) func(*verifierConfig) {
|
||||
return func(conf *verifierConfig) {
|
||||
conf.supportedSignAlgs = algs
|
||||
}
|
||||
}
|
||||
|
||||
type verifierConfig struct {
|
||||
issuer string
|
||||
clientID string
|
||||
nonce string
|
||||
iat *iatConfig
|
||||
acr ACRVerifier
|
||||
maxAge time.Duration
|
||||
supportedSignAlgs []string
|
||||
|
||||
// httpClient *http.Client
|
||||
|
||||
now time.Time
|
||||
}
|
||||
|
||||
type iatConfig struct {
|
||||
ignore bool
|
||||
offset time.Duration
|
||||
maxAge time.Duration
|
||||
}
|
||||
|
||||
//DefaultACRVerifier implements `ACRVerifier` returning an error
|
||||
//if non of the provided values matches the acr claim
|
||||
func DefaultACRVerifier(possibleValues []string) ACRVerifier {
|
||||
return func(acr string) error {
|
||||
if !utils.Contains(possibleValues, acr) {
|
||||
return ErrAcrInvalid(possibleValues, acr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
//Verify implements the `Verify` method of the `Verifier` interface
|
||||
//according to https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||
//and https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowTokenValidation
|
||||
func (v *DefaultVerifier) Verify(ctx context.Context, accessToken, idTokenString string) (*oidc.IDTokenClaims, error) {
|
||||
v.config.now = time.Now().UTC()
|
||||
idToken, err := v.VerifyIDToken(ctx, idTokenString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := v.verifyAccessToken(accessToken, idToken.AccessTokenHash, idToken.Signature); err != nil { //TODO: sig from token
|
||||
return nil, err
|
||||
}
|
||||
return idToken, nil
|
||||
}
|
||||
|
||||
func (v *DefaultVerifier) now() time.Time {
|
||||
if v.config.now.IsZero() {
|
||||
v.config.now = time.Now().UTC().Round(time.Second)
|
||||
}
|
||||
return v.config.now
|
||||
}
|
||||
|
||||
//VerifyIDToken: https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
|
||||
func (v *DefaultVerifier) VerifyIDToken(ctx context.Context, idTokenString string) (*oidc.IDTokenClaims, error) {
|
||||
//1. if encrypted --> decrypt
|
||||
decrypted, err := v.decryptToken(idTokenString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims, payload, err := v.parseToken(decrypted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// token, err := jwt.ParseWithClaims(decrypted, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
//2, check issuer (exact match)
|
||||
if err := v.checkIssuer(claims.Issuer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//3. check aud (aud must contain client_id, all aud strings must be allowed)
|
||||
if err = v.checkAudience(claims.Audiences); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = v.checkAuthorizedParty(claims.Audiences, claims.AuthorizedParty); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//6. check signature by keys
|
||||
//7. check alg default is rs256
|
||||
//8. check if alg is mac based (hs...) -> audience contains client_id. for validation use utf-8 representation of your client_secret
|
||||
claims.Signature, err = v.checkSignature(ctx, decrypted, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//9. check exp before now
|
||||
if err = v.checkExpiration(claims.Expiration); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//10. check iat duration is optional (can be checked)
|
||||
if err = v.checkIssuedAt(claims.IssuedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//11. check nonce (check if optional possible) id_token.nonce == sentNonce
|
||||
if err = v.checkNonce(claims.Nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//12. if acr requested check acr
|
||||
if err = v.checkAuthorizationContextClassReference(claims.AuthenticationContextClassReference); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//13. if auth_time requested check if auth_time is less than max age
|
||||
if err = v.checkAuthTime(claims.AuthTime); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func (v *DefaultVerifier) parseToken(tokenString string) (*oidc.IDTokenClaims, []byte, error) {
|
||||
parts := strings.Split(tokenString, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, nil, ValidationError("token contains an invalid number of segments") //TODO: err NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("oidc: malformed jwt payload: %v", err)
|
||||
}
|
||||
idToken := new(oidc.IDTokenClaims)
|
||||
err = json.Unmarshal(payload, idToken)
|
||||
return idToken, payload, err
|
||||
}
|
||||
|
||||
func (v *DefaultVerifier) checkIssuer(issuer string) error {
|
||||
if v.config.issuer != issuer {
|
||||
return ErrIssuerInvalid(v.config.issuer, issuer)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *DefaultVerifier) checkAudience(audiences []string) error {
|
||||
if !utils.Contains(audiences, v.config.clientID) {
|
||||
return ErrAudienceMissingClientID(v.config.clientID)
|
||||
}
|
||||
|
||||
//TODO: check aud trusted
|
||||
return nil
|
||||
}
|
||||
|
||||
//4. if multiple aud strings --> check if azp
|
||||
//5. if azp --> check azp == client_id
|
||||
func (v *DefaultVerifier) checkAuthorizedParty(audiences []string, authorizedParty string) error {
|
||||
if len(audiences) > 1 {
|
||||
if authorizedParty == "" {
|
||||
return ErrAzpMissing()
|
||||
}
|
||||
}
|
||||
if authorizedParty != "" && authorizedParty != v.config.clientID {
|
||||
return ErrAzpInvalid(authorizedParty, v.config.clientID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *DefaultVerifier) checkSignature(ctx context.Context, idTokenString string, payload []byte) (jose.SignatureAlgorithm, error) {
|
||||
jws, err := jose.ParseSigned(idTokenString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(jws.Signatures) == 0 {
|
||||
return "", nil //TODO: error
|
||||
}
|
||||
if len(jws.Signatures) > 1 {
|
||||
return "", nil //TODO: error
|
||||
}
|
||||
sig := jws.Signatures[0]
|
||||
supportedSigAlgs := v.config.supportedSignAlgs
|
||||
if len(supportedSigAlgs) == 0 {
|
||||
supportedSigAlgs = []string{"RS256"}
|
||||
}
|
||||
if !utils.Contains(supportedSigAlgs, sig.Header.Algorithm) {
|
||||
return "", fmt.Errorf("oidc: id token signed with unsupported algorithm, expected %q got %q", supportedSigAlgs, sig.Header.Algorithm)
|
||||
}
|
||||
|
||||
signedPayload, err := v.keySet.VerifySignature(ctx, jws)
|
||||
if err != nil {
|
||||
return "", err
|
||||
//TODO:
|
||||
}
|
||||
|
||||
if !bytes.Equal(signedPayload, payload) {
|
||||
return "", ErrSignatureInvalidPayload() //TODO: err
|
||||
}
|
||||
return jose.SignatureAlgorithm(sig.Header.Algorithm), nil
|
||||
}
|
||||
|
||||
func (v *DefaultVerifier) checkExpiration(expiration time.Time) error {
|
||||
expiration = expiration.Round(time.Second)
|
||||
if !v.now().Before(expiration) {
|
||||
return ErrExpInvalid(expiration)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *DefaultVerifier) checkIssuedAt(issuedAt time.Time) error {
|
||||
if v.config.iat.ignore {
|
||||
return nil
|
||||
}
|
||||
issuedAt = issuedAt.Round(time.Second)
|
||||
offset := v.now().Add(v.config.iat.offset).Round(time.Second)
|
||||
if issuedAt.After(offset) {
|
||||
return ErrIatInFuture(issuedAt, offset)
|
||||
}
|
||||
if v.config.iat.maxAge == 0 {
|
||||
return nil
|
||||
}
|
||||
maxAge := v.now().Add(-v.config.iat.maxAge).Round(time.Second)
|
||||
if issuedAt.Before(maxAge) {
|
||||
return ErrIatToOld(maxAge, issuedAt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (v *DefaultVerifier) checkNonce(nonce string) error {
|
||||
if v.config.nonce == "" {
|
||||
return nil
|
||||
}
|
||||
if v.config.nonce != nonce {
|
||||
return ErrNonceInvalid(v.config.nonce, nonce)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (v *DefaultVerifier) checkAuthorizationContextClassReference(acr string) error {
|
||||
if v.config.acr != nil {
|
||||
return v.config.acr(acr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (v *DefaultVerifier) checkAuthTime(authTime time.Time) error {
|
||||
if v.config.maxAge == 0 {
|
||||
return nil
|
||||
}
|
||||
if authTime.IsZero() {
|
||||
return ErrAuthTimeNotPresent()
|
||||
}
|
||||
authTime = authTime.Round(time.Second)
|
||||
maxAge := v.now().Add(-v.config.maxAge).Round(time.Second)
|
||||
if authTime.Before(maxAge) {
|
||||
return ErrAuthTimeToOld(maxAge, authTime)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *DefaultVerifier) decryptToken(tokenString string) (string, error) {
|
||||
return tokenString, nil //TODO: impl
|
||||
}
|
||||
|
||||
func (v *DefaultVerifier) verifyAccessToken(accessToken, atHash string, sigAlgorithm jose.SignatureAlgorithm) error {
|
||||
if atHash == "" {
|
||||
return nil //TODO: return error
|
||||
}
|
||||
|
||||
actual, err := oidc.ClaimHash(accessToken, sigAlgorithm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if actual != atHash {
|
||||
return nil //TODO: error
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue