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