FEATURES (and FIXES): - support OAuth 2.0 Token Revocation [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009) - handle request object using `request` parameter [OIDC Core 1.0 Request Object](https://openid.net/specs/openid-connect-core-1_0.html#RequestObject) - handle response mode - added some information to the discovery endpoint: - revocation_endpoint (added with token revocation) - revocation_endpoint_auth_methods_supported (added with token revocation) - revocation_endpoint_auth_signing_alg_values_supported (added with token revocation) - token_endpoint_auth_signing_alg_values_supported (was missing) - introspection_endpoint_auth_signing_alg_values_supported (was missing) - request_object_signing_alg_values_supported (added with request object) - request_parameter_supported (added with request object) - fixed `removeUserinfoScopes ` now returns the scopes without "userinfo" scopes (profile, email, phone, addedd) [source diff](https://github.com/caos/oidc/pull/130/files#diff-fad50c8c0f065d4dbc49d6c6a38f09c992c8f5d651a479ba00e31b500543559eL170-R171) - improved error handling (pkg/oidc/error.go) and fixed some wrong OAuth errors (e.g. `invalid_grant` instead of `invalid_request`) - improved MarshalJSON and added MarshalJSONWithStatus - removed deprecated PEM decryption from `BytesToPrivateKey` [source diff](https://github.com/caos/oidc/pull/130/files#diff-fe246e428e399ccff599627c71764de51387b60b4df84c67de3febd0954e859bL11-L19) - NewAccessTokenVerifier now uses correct (internal) `accessTokenVerifier` [source diff](https://github.com/caos/oidc/pull/130/files#diff-3a01c7500ead8f35448456ef231c7c22f8d291710936cac91de5edeef52ffc72L52-R52) BREAKING CHANGE: - move functions from `utils` package into separate packages - added various methods to the (OP) `Configuration` interface [source diff](https://github.com/caos/oidc/pull/130/files#diff-2538e0dfc772fdc37f057aecd6fcc2943f516c24e8be794cce0e368a26d20a82R19-R32) - added revocationEndpoint to `WithCustomEndpoints ` [source diff](https://github.com/caos/oidc/pull/130/files#diff-19ae13a743eb7cebbb96492798b1bec556673eb6236b1387e38d722900bae1c3L355-R391) - remove unnecessary context parameter from JWTProfileExchange [source diff](https://github.com/caos/oidc/pull/130/files#diff-4ed8f6affa4a9631fa8a034b3d5752fbb6a819107141aae00029014e950f7b4cL14)
218 lines
6.8 KiB
Go
218 lines
6.8 KiB
Go
package oidc
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"gopkg.in/square/go-jose.v2"
|
|
|
|
str "github.com/caos/oidc/pkg/strings"
|
|
)
|
|
|
|
type Claims interface {
|
|
GetIssuer() string
|
|
GetSubject() string
|
|
GetAudience() []string
|
|
GetExpiration() time.Time
|
|
GetIssuedAt() time.Time
|
|
GetNonce() string
|
|
GetAuthenticationContextClassReference() string
|
|
GetAuthTime() time.Time
|
|
GetAuthorizedParty() string
|
|
ClaimsSignature
|
|
}
|
|
|
|
type ClaimsSignature interface {
|
|
SetSignatureAlgorithm(algorithm jose.SignatureAlgorithm)
|
|
}
|
|
|
|
var (
|
|
ErrParse = errors.New("parsing of request failed")
|
|
ErrIssuerInvalid = errors.New("issuer does not match")
|
|
ErrSubjectMissing = errors.New("subject missing")
|
|
ErrAudience = errors.New("audience is not valid")
|
|
ErrAzpMissing = errors.New("authorized party is not set. If Token is valid for multiple audiences, azp must not be empty")
|
|
ErrAzpInvalid = errors.New("authorized party is not valid")
|
|
ErrSignatureMissing = errors.New("id_token does not contain a signature")
|
|
ErrSignatureMultiple = errors.New("id_token contains multiple signatures")
|
|
ErrSignatureUnsupportedAlg = errors.New("signature algorithm not supported")
|
|
ErrSignatureInvalidPayload = errors.New("signature does not match Payload")
|
|
ErrSignatureInvalid = errors.New("invalid signature")
|
|
ErrExpired = errors.New("token has expired")
|
|
ErrIatMissing = errors.New("issuedAt of token is missing")
|
|
ErrIatInFuture = errors.New("issuedAt of token is in the future")
|
|
ErrIatToOld = errors.New("issuedAt of token is to old")
|
|
ErrNonceInvalid = errors.New("nonce does not match")
|
|
ErrAcrInvalid = errors.New("acr is invalid")
|
|
ErrAuthTimeNotPresent = errors.New("claim `auth_time` of token is missing")
|
|
ErrAuthTimeToOld = errors.New("auth time of token is to old")
|
|
ErrAtHash = errors.New("at_hash does not correspond to access token")
|
|
)
|
|
|
|
type Verifier interface {
|
|
Issuer() string
|
|
MaxAgeIAT() time.Duration
|
|
Offset() time.Duration
|
|
}
|
|
|
|
//ACRVerifier specifies the function to be used by the `DefaultVerifier` for validating the acr claim
|
|
type ACRVerifier func(string) error
|
|
|
|
//DefaultACRVerifier implements `ACRVerifier` returning an error
|
|
//if none of the provided values matches the acr claim
|
|
func DefaultACRVerifier(possibleValues []string) ACRVerifier {
|
|
return func(acr string) error {
|
|
if !str.Contains(possibleValues, acr) {
|
|
return fmt.Errorf("expected one of: %v, got: %q", possibleValues, acr)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func DecryptToken(tokenString string) (string, error) {
|
|
return tokenString, nil //TODO: impl
|
|
}
|
|
|
|
func ParseToken(tokenString string, claims interface{}) ([]byte, error) {
|
|
parts := strings.Split(tokenString, ".")
|
|
if len(parts) != 3 {
|
|
return nil, fmt.Errorf("%w: token contains an invalid number of segments", ErrParse)
|
|
}
|
|
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: malformed jwt payload: %v", ErrParse, err)
|
|
}
|
|
err = json.Unmarshal(payload, claims)
|
|
return payload, err
|
|
}
|
|
|
|
func CheckSubject(claims Claims) error {
|
|
if claims.GetSubject() == "" {
|
|
return ErrSubjectMissing
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func CheckIssuer(claims Claims, issuer string) error {
|
|
if claims.GetIssuer() != issuer {
|
|
return fmt.Errorf("%w: Expected: %s, got: %s", ErrIssuerInvalid, issuer, claims.GetIssuer())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func CheckAudience(claims Claims, clientID string) error {
|
|
if !str.Contains(claims.GetAudience(), clientID) {
|
|
return fmt.Errorf("%w: Audience must contain client_id %q", ErrAudience, clientID)
|
|
}
|
|
|
|
//TODO: check aud trusted
|
|
return nil
|
|
}
|
|
|
|
func CheckAuthorizedParty(claims Claims, clientID string) error {
|
|
if len(claims.GetAudience()) > 1 {
|
|
if claims.GetAuthorizedParty() == "" {
|
|
return ErrAzpMissing
|
|
}
|
|
}
|
|
if claims.GetAuthorizedParty() != "" && claims.GetAuthorizedParty() != clientID {
|
|
return fmt.Errorf("%w: azp %q must be equal to client_id %q", ErrAzpInvalid, claims.GetAuthorizedParty(), clientID)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func CheckSignature(ctx context.Context, token string, payload []byte, claims ClaimsSignature, supportedSigAlgs []string, set KeySet) error {
|
|
jws, err := jose.ParseSigned(token)
|
|
if err != nil {
|
|
return ErrParse
|
|
}
|
|
if len(jws.Signatures) == 0 {
|
|
return ErrSignatureMissing
|
|
}
|
|
if len(jws.Signatures) > 1 {
|
|
return ErrSignatureMultiple
|
|
}
|
|
sig := jws.Signatures[0]
|
|
if len(supportedSigAlgs) == 0 {
|
|
supportedSigAlgs = []string{"RS256"}
|
|
}
|
|
if !str.Contains(supportedSigAlgs, sig.Header.Algorithm) {
|
|
return fmt.Errorf("%w: id token signed with unsupported algorithm, expected %q got %q", ErrSignatureUnsupportedAlg, supportedSigAlgs, sig.Header.Algorithm)
|
|
}
|
|
|
|
signedPayload, err := set.VerifySignature(ctx, jws)
|
|
if err != nil {
|
|
return fmt.Errorf("%w (%v)", ErrSignatureInvalid, err)
|
|
}
|
|
|
|
if !bytes.Equal(signedPayload, payload) {
|
|
return ErrSignatureInvalidPayload
|
|
}
|
|
|
|
claims.SetSignatureAlgorithm(jose.SignatureAlgorithm(sig.Header.Algorithm))
|
|
|
|
return nil
|
|
}
|
|
|
|
func CheckExpiration(claims Claims, offset time.Duration) error {
|
|
expiration := claims.GetExpiration().Round(time.Second)
|
|
if !time.Now().UTC().Add(offset).Before(expiration) {
|
|
return ErrExpired
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func CheckIssuedAt(claims Claims, maxAgeIAT, offset time.Duration) error {
|
|
issuedAt := claims.GetIssuedAt().Round(time.Second)
|
|
if issuedAt.IsZero() {
|
|
return ErrIatMissing
|
|
}
|
|
nowWithOffset := time.Now().UTC().Add(offset).Round(time.Second)
|
|
if issuedAt.After(nowWithOffset) {
|
|
return fmt.Errorf("%w: (iat: %v, now with offset: %v)", ErrIatInFuture, issuedAt, nowWithOffset)
|
|
}
|
|
if maxAgeIAT == 0 {
|
|
return nil
|
|
}
|
|
maxAge := time.Now().UTC().Add(-maxAgeIAT).Round(time.Second)
|
|
if issuedAt.Before(maxAge) {
|
|
return fmt.Errorf("%w: must not be older than %v, but was %v (%v to old)", ErrIatToOld, maxAge, issuedAt, maxAge.Sub(issuedAt))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func CheckNonce(claims Claims, nonce string) error {
|
|
if claims.GetNonce() != nonce {
|
|
return fmt.Errorf("%w: expected %q but was %q", ErrNonceInvalid, nonce, claims.GetNonce())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func CheckAuthorizationContextClassReference(claims Claims, acr ACRVerifier) error {
|
|
if acr != nil {
|
|
if err := acr(claims.GetAuthenticationContextClassReference()); err != nil {
|
|
return fmt.Errorf("%w: %v", ErrAcrInvalid, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
func CheckAuthTime(claims Claims, maxAge time.Duration) error {
|
|
if maxAge == 0 {
|
|
return nil
|
|
}
|
|
if claims.GetAuthTime().IsZero() {
|
|
return ErrAuthTimeNotPresent
|
|
}
|
|
authTime := claims.GetAuthTime().Round(time.Second)
|
|
maxAuthTime := time.Now().UTC().Add(-maxAge).Round(time.Second)
|
|
if authTime.Before(maxAuthTime) {
|
|
return fmt.Errorf("%w: must not be older than %v, but was %v (%v to old)", ErrAuthTimeToOld, maxAge, authTime, maxAuthTime.Sub(authTime))
|
|
}
|
|
return nil
|
|
}
|