Repository: smallstep/cli
Pull request #953 (2023-06-06 17:54:13)

Detect OIDC tokens issued by Kubernetes
Service account tokens issued by the Kubernetes API server in recent versions are valid OIDC tokens but do not contain an email in the payload. Update the Payload Type test to interpret tokens with [all five required claims](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) as OIDC tokens even if they do not have an email claim.

An example Kubernetes OIDC token:
```json
{
  "aud": [
    "step-ca"
  ],
  "exp": 1686076066,
  "iat": 1686072466,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "kubernetes.io": {
    "namespace": "default",
    "pod": {
      "name": "pod",
      "uid": "98844dbc-d248-4ddb-b385-81643892908d"
    },
    "serviceaccount": {
      "name": "default",
      "uid": "e79de63f-8842-46bc-9931-c277c4993994"
    }
  },
  "nbf": 1686072466,
  "sub": "system:serviceaccount:default:default"
}
```
<!---
Please provide answers in the spaces below each prompt, where applicable.
Not every PR requires responses for each prompt.
Use your discretion.
-->
#### Name of feature:
Detect OIDC tokens without email claim.

#### Pain or issue this feature alleviates:
OIDC tokens issued by Kubernetes are interpreted as type unknown and trying to get a certificate results in error `token is not supported`.

#### Why is this important to the project (if not answered above):

#### Is there documentation on how to use this feature? If so, where?

#### In what environments or workflows is this feature supported?

#### In what environments or workflows is this feature explicitly NOT supported (if any)?

#### Supporting links/other PRs/issues:

💔Thank you!



Patch:
diff --git a/token/parse.go b/token/parse.go
index 84897875..3da26a21 100644
--- a/token/parse.go
+++ b/token/parse.go
@@ -74,7 +74,7 @@ func (p Payload) Type() Type {
 		return K8sSA
 	case len(p.SHA) > 0 || len(p.SANs) > 0:
 		return JWK
-	case p.Email != "":
+	case len(p.Audience) > 0 && p.Issuer != "" && p.Subject != "" && !p.Expiry.Time().IsZero() && !p.IssuedAt.Time().IsZero():
 		return OIDC
 	default:
 		return Unknown


Test patch:
diff --git a/token/parse_test.go b/token/parse_test.go
index 4866b707..6a2ed099 100644
--- a/token/parse_test.go
+++ b/token/parse_test.go
@@ -286,35 +286,37 @@ func TestPayload_Type(t *testing.T) {
 	type fields struct {
 		SHA    string
 		SANs   []string
-		Email  string
 		Google *GCPGooglePayload
 		Amazon *AWSAmazonPayload
 		Azure  *AzurePayload
+		Claims *jose.Claims
 	}
 	tests := []struct {
 		name   string
 		fields fields
 		want   Type
 	}{
-		{"JWK", fields{"a-sha", []string{"foo.bar.zar"}, "", nil, nil, nil}, JWK},
-		{"JWK no sans", fields{"a-sha", nil, "", nil, nil, nil}, JWK},
-		{"JWK no sha", fields{"", []string{"foo.bar.zar"}, "", nil, nil, nil}, JWK},
-		{"OIDC", fields{"", nil, "mariano@smallstep.com", nil, nil, nil}, OIDC},
-		{"GCP", fields{"", nil, "", &GCPGooglePayload{}, nil, nil}, GCP},
-		{"AWS", fields{"", nil, "", nil, &AWSAmazonPayload{}, nil}, AWS},
-		{"Azure", fields{"", nil, "", nil, nil, &AzurePayload{}}, Azure},
-		{"Unknown", fields{"", nil, "", nil, nil, nil}, Unknown},
+		{"JWK", fields{"a-sha", []string{"foo.bar.zar"}, nil, nil, nil, nil}, JWK},
+		{"JWK no sans", fields{"a-sha", nil, nil, nil, nil, nil}, JWK},
+		{"JWK no sha", fields{"", []string{"foo.bar.zar"}, nil, nil, nil, nil}, JWK},
+		{"GCP", fields{"", nil, &GCPGooglePayload{}, nil, nil, nil}, GCP},
+		{"AWS", fields{"", nil, nil, &AWSAmazonPayload{}, nil, nil}, AWS},
+		{"Azure", fields{"", nil, nil, nil, &AzurePayload{}, nil}, Azure},
+		{"Unknown", fields{"", nil, nil, nil, nil, nil}, Unknown},
+		{"OIDC Kubernetes", fields{"", nil, nil, nil, nil, &jose.Claims{Audience: jose.Audience{"step-ca"}, Issuer: "https://kubernetes.default.svc.cluster.local", Subject: "system:serviceaccount:default:default", Expiry: jose.NewNumericDate(time.Now()), IssuedAt: jose.NewNumericDate(time.Now())}}, OIDC},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			p := Payload{
 				SHA:    tt.fields.SHA,
 				SANs:   tt.fields.SANs,
-				Email:  tt.fields.Email,
 				Google: tt.fields.Google,
 				Amazon: tt.fields.Amazon,
 				Azure:  tt.fields.Azure,
 			}
+			if tt.fields.Claims != nil {
+				p.Claims = *tt.fields.Claims
+			}
 			if got := p.Type(); got != tt.want {
 				t.Errorf("Payload.Type() = %v, want %v", got, tt.want)
 			}