Skip to content

Commit

Permalink
Added federated OIDC JWT support. (#680)
Browse files Browse the repository at this point in the history
* Added federated OIDC JWT support.

* Fixing go formatting.

* Made JWT field for federated identities internal

Co-authored-by: Joel Hendrix <jhendrix@microsoft.com>
  • Loading branch information
JustinJudd and jhendrixMSFT committed May 4, 2022
1 parent 7525a9b commit 0dbcd58
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 0 deletions.
46 changes: 46 additions & 0 deletions autorest/adal/token.go
Expand Up @@ -365,6 +365,25 @@ func (secret ServicePrincipalAuthorizationCodeSecret) MarshalJSON() ([]byte, err
})
}

// ServicePrincipalFederatedSecret implements ServicePrincipalSecret for Federated JWTs.
type ServicePrincipalFederatedSecret struct {
jwt string
}

// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
// It will populate the form submitted during OAuth Token Acquisition using a JWT signed by an OIDC issuer.
func (secret *ServicePrincipalFederatedSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {

v.Set("client_assertion", secret.jwt)
v.Set("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
return nil
}

// MarshalJSON implements the json.Marshaler interface.
func (secret ServicePrincipalFederatedSecret) MarshalJSON() ([]byte, error) {
return nil, errors.New("marshalling ServicePrincipalFederatedSecret is not supported")
}

// ServicePrincipalToken encapsulates a Token created for a Service Principal.
type ServicePrincipalToken struct {
inner servicePrincipalToken
Expand Down Expand Up @@ -419,6 +438,8 @@ func (spt *ServicePrincipalToken) UnmarshalJSON(data []byte) error {
spt.inner.Secret = &ServicePrincipalUsernamePasswordSecret{}
case "ServicePrincipalAuthorizationCodeSecret":
spt.inner.Secret = &ServicePrincipalAuthorizationCodeSecret{}
case "ServicePrincipalFederatedSecret":
return errors.New("unmarshalling ServicePrincipalFederatedSecret is not supported")
default:
return fmt.Errorf("unrecognized token type '%s'", secret["type"])
}
Expand Down Expand Up @@ -665,6 +686,31 @@ func NewServicePrincipalTokenFromAuthorizationCode(oauthConfig OAuthConfig, clie
)
}

// NewServicePrincipalTokenFromFederatedToken creates a ServicePrincipalToken from the supplied federated OIDC JWT.
func NewServicePrincipalTokenFromFederatedToken(oauthConfig OAuthConfig, clientID string, jwt string, resource string, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
if err := validateOAuthConfig(oauthConfig); err != nil {
return nil, err
}
if err := validateStringParam(clientID, "clientID"); err != nil {
return nil, err
}
if err := validateStringParam(resource, "resource"); err != nil {
return nil, err
}
if jwt == "" {
return nil, fmt.Errorf("parameter 'jwt' cannot be empty")
}
return NewServicePrincipalTokenWithSecret(
oauthConfig,
clientID,
resource,
&ServicePrincipalFederatedSecret{
jwt: jwt,
},
callbacks...,
)
}

type msiType int

const (
Expand Down
72 changes: 72 additions & 0 deletions autorest/adal/token_test.go
Expand Up @@ -700,6 +700,46 @@ func TestServicePrincipalTokenSecretRefreshSetsBody(t *testing.T) {
})
}

func TestServicePrincipalTokenFederatedJwtRefreshSetsBody(t *testing.T) {
sptCert := newServicePrincipalTokenFederatedJwt(t)
testServicePrincipalTokenRefreshSetsBody(t, sptCert, func(t *testing.T, b []byte) {
body := string(b)

values, _ := url.ParseQuery(body)
if values["client_assertion_type"][0] != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" ||
values["client_id"][0] != "id" ||
values["grant_type"][0] != "client_credentials" ||
values["resource"][0] != "resource" {
t.Fatalf("adal: ServicePrincipalTokenCertificate#Refresh did not correctly set the HTTP Request Body.")
}

tok, _ := jwt.Parse(values["client_assertion"][0], nil)
if tok == nil {
t.Fatalf("adal: ServicePrincipalTokenCertificate#Expected client_assertion to be a JWT")
}
if _, ok := tok.Header["typ"]; !ok {
t.Fatalf("adal: ServicePrincipalTokenCertificate#Expected client_assertion to have an typ header")
}

claims, ok := tok.Claims.(jwt.MapClaims)
if !ok {
t.Fatalf("expected MapClaims, got %T", tok.Claims)
}
if err := claims.Valid(); err != nil {
t.Fatalf("invalid claim: %v", err)
}
if aud := claims["aud"]; aud != "testAudience" {
t.Fatalf("unexpected aud: %s", aud)
}
if iss := claims["iss"]; iss != "id" {
t.Fatalf("unexpected iss: %s", iss)
}
if sub := claims["sub"]; sub != "id" {
t.Fatalf("unexpected sub: %s", sub)
}
})
}

func TestServicePrincipalTokenRefreshClosesRequestBody(t *testing.T) {
spt := newServicePrincipalToken()

Expand Down Expand Up @@ -1266,6 +1306,19 @@ func TestMarshalServicePrincipalAuthorizationCodeSecret(t *testing.T) {
}
}

func TestMarshalServicePrincipalFederatedSecret(t *testing.T) {
spt := newServicePrincipalTokenFederatedJwt(t)
b, err := json.Marshal(spt)
if err == nil {
t.Fatal("expected error when marshalling certificate token")
}
var spt2 *ServicePrincipalToken
err = json.Unmarshal(b, &spt2)
if err == nil {
t.Fatal("expected error when unmarshalling certificate token")
}
}

func TestMarshalInnerToken(t *testing.T) {
spt := newServicePrincipalTokenManual()
tokenJSON, err := spt.MarshalTokenJSON()
Expand Down Expand Up @@ -1465,3 +1518,22 @@ func newServicePrincipalTokenAuthorizationCode(t *testing.T) *ServicePrincipalTo
spt, _ := NewServicePrincipalTokenFromAuthorizationCode(TestOAuthConfig, "id", "clientSecret", "code", "http://redirectUri/getToken", "resource")
return spt
}

func newServicePrincipalTokenFederatedJwt(t *testing.T) *ServicePrincipalToken {
token := jwt.New(jwt.SigningMethodHS256)
token.Header["typ"] = "JWT"
token.Claims = jwt.MapClaims{
"aud": "testAudience",
"iss": "id",
"sub": "id",
"nbf": time.Now().Unix(),
"exp": time.Now().Add(24 * time.Hour).Unix(),
}

signedString, err := token.SignedString([]byte("test key"))
if err != nil {
t.Fatal(err)
}
spt, _ := NewServicePrincipalTokenFromFederatedToken(TestOAuthConfig, "id", signedString, "resource")
return spt
}

0 comments on commit 0dbcd58

Please sign in to comment.