blob: c3570a310fe55a3e8545fcb069819a5548fc7396 [file] [log] [blame]
// Copyright 2019 Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package authenticate
import (
"context"
"fmt"
"net/http"
"strings"
)
import (
oidc "github.com/coreos/go-oidc/v3/oidc"
"istio.io/api/security/v1beta1"
)
import (
"github.com/apache/dubbo-go-pixiu/pkg/security"
)
const (
IDTokenAuthenticatorType = "IDTokenAuthenticator"
)
type JwtAuthenticator struct {
trustDomain string
audiences []string
verifier *oidc.IDTokenVerifier
}
var _ security.Authenticator = &JwtAuthenticator{}
// newJwtAuthenticator is used when running istiod outside of a cluster, to validate the tokens using OIDC
// K8S is created with --service-account-issuer, service-account-signing-key-file and service-account-api-audiences
// which enable OIDC.
func NewJwtAuthenticator(jwtRule *v1beta1.JWTRule, trustDomain string) (*JwtAuthenticator, error) {
issuer := jwtRule.GetIssuer()
jwksURL := jwtRule.GetJwksUri()
// The key of a JWT issuer may change, so the key may need to be updated.
// Based on https://pkg.go.dev/github.com/coreos/go-oidc/v3/oidc#NewRemoteKeySet
// the oidc library handles caching and cache invalidation. Thus, the verifier
// is only created once in the constructor.
var verifier *oidc.IDTokenVerifier
if len(jwksURL) == 0 {
// OIDC discovery is used if jwksURL is not set.
provider, err := oidc.NewProvider(context.Background(), issuer)
// OIDC discovery may fail, e.g. http request for the OIDC server may fail.
if err != nil {
return nil, fmt.Errorf("failed at creating an OIDC provider for %v: %v", issuer, err)
}
verifier = provider.Verifier(&oidc.Config{SkipClientIDCheck: true})
} else {
keySet := oidc.NewRemoteKeySet(context.Background(), jwksURL)
verifier = oidc.NewVerifier(issuer, keySet, &oidc.Config{SkipClientIDCheck: true})
}
return &JwtAuthenticator{
trustDomain: trustDomain,
verifier: verifier,
audiences: jwtRule.Audiences,
}, nil
}
func (j *JwtAuthenticator) AuthenticateRequest(req *http.Request) (*security.Caller, error) {
targetJWT, err := security.ExtractRequestToken(req)
if err != nil {
return nil, fmt.Errorf("target JWT extraction error: %v", err)
}
return j.authenticate(req.Context(), targetJWT)
}
// Authenticate - based on the old OIDC authenticator for mesh expansion.
func (j *JwtAuthenticator) Authenticate(ctx context.Context) (*security.Caller, error) {
bearerToken, err := security.ExtractBearerToken(ctx)
if err != nil {
return nil, fmt.Errorf("ID token extraction error: %v", err)
}
return j.authenticate(ctx, bearerToken)
}
func (j *JwtAuthenticator) authenticate(ctx context.Context, bearerToken string) (*security.Caller, error) {
idToken, err := j.verifier.Verify(ctx, bearerToken)
if err != nil {
return nil, fmt.Errorf("failed to verify the JWT token (error %v)", err)
}
sa := JwtPayload{}
// "aud" for trust domain, "sub" has "system:serviceaccount:$namespace:$serviceaccount".
// in future trust domain may use another field as a standard is defined.
if err := idToken.Claims(&sa); err != nil {
return nil, fmt.Errorf("failed to extract claims from ID token: %v", err)
}
if !strings.HasPrefix(sa.Sub, "system:serviceaccount") {
return nil, fmt.Errorf("invalid sub %v", sa.Sub)
}
parts := strings.Split(sa.Sub, ":")
ns := parts[2]
ksa := parts[3]
if !checkAudience(sa.Aud, j.audiences) {
return nil, fmt.Errorf("invalid audiences %v", sa.Aud)
}
return &security.Caller{
AuthSource: security.AuthSourceIDToken,
Identities: []string{fmt.Sprintf(IdentityTemplate, j.trustDomain, ns, ksa)},
}, nil
}
// checkAudience() returns true if the audiences to check are in
// the expected audiences. Otherwise, return false.
func checkAudience(audToCheck []string, audExpected []string) bool {
for _, a := range audToCheck {
for _, b := range audExpected {
if a == b {
return true
}
}
}
return false
}
type JwtPayload struct {
// Aud is the expected audience, defaults to istio-ca - but is based on istiod.yaml configuration.
// If set to a different value - use the value defined by istiod.yaml. Env variable can
// still override
Aud []string `json:"aud"`
// Exp is not currently used - we don't use the token for authn, just to determine k8s settings
Exp int `json:"exp"`
// Issuer - configured by K8S admin for projected tokens. Will be used to verify all tokens.
Iss string `json:"iss"`
Sub string `json:"sub"`
}
func (j JwtAuthenticator) AuthenticatorType() string {
return IDTokenAuthenticatorType
}