Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.azure.spring.aad.resource;


import com.azure.spring.aad.implementation.IdentityEndpoints;
import com.azure.spring.aad.resource.validator.AzureJwtAudienceValidator;
import com.azure.spring.aad.resource.validator.AzureJwtIssuerValidator;
import com.azure.spring.aad.resource.validator.AzureJwtTenantValidator;
import com.azure.spring.autoconfigure.aad.AADAuthenticationProperties;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(AADAuthenticationProperties.class)
@ConditionalOnClass(name = {"org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken"})
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(prefix = "azure.activedirectory", value = {"client-id", "client-secret", "tenant-id"})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need these properties?

public class AzureResourceServerAutoConfiguration {

@Autowired
private AADAuthenticationProperties aadAuthenticationProperties;

@Bean
@ConditionalOnProperty(prefix = "azure.activedirectory.resource", value = {"client-id", "tenant-id",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these properties are not required right?

If tenant id is provided, we will construct the jwkset uri from the tenant id, otherwise we could use common?
If no client-id or app-id-uri is provided, we could skip the audience check?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All these properties are not required right?
yes.

If tenant id is provided, we will construct the jwkset uri from the tenant id, otherwise we could use common?
If no client-id or app-id-uri is provided, we could skip the audience check?

Yes, you are right.

"app-id-uri"})
@ConditionalOnMissingBean(JwtDecoder.class)
JwtDecoder jwtDecoderByJwkKeySetUri() {
IdentityEndpoints endpoints = new IdentityEndpoints(aadAuthenticationProperties.getUri());
NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see why the tenant id is required here, but why this prefix azure.activedirectory.resource is used here?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So for the case of single-tenant app, the tenant id will be its tenant id. For multi-tenant app, it will be common?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

about azure.activedirectory.resource ,a hand by mistake,it will be modified.
So for the case of single-tenant app, the tenant id will be its tenant id. For multi-tenant app, it will be common?
yes

.withJwkSetUri(endpoints.jwkSetEndpoint(aadAuthenticationProperties.getTenantId())).build();
List<OAuth2TokenValidator<Jwt>> validators = createDefaultValidator();
nimbusJwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators));
return nimbusJwtDecoder;
}

//TODO There can be other jwtDecoder generation methods

private List<OAuth2TokenValidator<Jwt>> createDefaultValidator() {
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
List<String> validAudiences = new ArrayList<>();
validAudiences.add(aadAuthenticationProperties.getClientId());
validAudiences.add(aadAuthenticationProperties.getAppIdUri());
validators.add(new AzureJwtIssuerValidator(aadAuthenticationProperties.getTenantId(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we using tenant ids to construct the issuer validator?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the issuer in the form of "https://sts.windows.net/4acebda4-c077-434a-8675-c4b5afebc8da/", so I hope that by passing tenantId to construct corresponding issuer, then the access token of the 'iss' of claims to match.

aadAuthenticationProperties.getAllowedTenantIds()));
validators.add(new AzureJwtAudienceValidator(validAudiences));
validators.add(new AzureJwtTenantValidator(aadAuthenticationProperties.getTenantId(),
aadAuthenticationProperties.getAllowedTenantIds()));
validators.add(new JwtTimestampValidator());
return validators;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.azure.spring.aad.resource.validator;

import com.azure.spring.autoconfigure.aad.AADTokenClaim;
import java.util.List;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
import org.springframework.util.Assert;

public class AzureJwtAudienceValidator implements OAuth2TokenValidator<Jwt> {


private final JwtClaimValidator<List<String>> validator;

public AzureJwtAudienceValidator(List<String> audiences) {
Assert.notNull(audiences, "audiences cannot be null");
this.validator = new JwtClaimValidator(AADTokenClaim.AUD, aud -> audiences.containsAll((List<String>) aud));
}

/**
* {@inheritDoc}
*/
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
Assert.notNull(token, "token cannot be null");
return this.validator.validate(token);
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.azure.spring.aad.resource.validator;

import com.azure.spring.autoconfigure.aad.AADTokenClaim;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

public class AzureJwtIssuerValidator implements OAuth2TokenValidator<Jwt> {

private static final String LOGIN_MICROSOFT_ONLINE_ISSUER = "https://login.microsoftonline.com/";
private static final String STS_WINDOWS_ISSUER = "https://sts.windows.net/";
private static final String STS_CHINA_CLOUD_API_ISSUER = "https://sts.chinacloudapi.cn/";
private final JwtClaimValidator<String> validator;

public AzureJwtIssuerValidator(String tenantId, Set<String> allowedTenantIds) {
Assert.notNull(tenantId, "tenantId cannot be null");
Assert.notNull(allowedTenantIds, "allowedTenantIds cannot be null");
if (allowedTenantIds.isEmpty()) {
if (StringUtils.isEmpty(tenantId)) {
this.validator = new JwtClaimValidator(AADTokenClaim.ISS, iss -> true);
} else {
List<String> issuers = assembleIssuer(tenantId);
this.validator = new JwtClaimValidator(AADTokenClaim.ISS, issuers::contains);
}
} else {
this.validator = new JwtClaimValidator(AADTokenClaim.ISS, validIssuer(allowedTenantIds));
}
}

private List<String> assembleIssuer(String tenantId) {
List<String> issuers = new ArrayList<>();
issuers.add(LOGIN_MICROSOFT_ONLINE_ISSUER + tenantId + "/");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check the tenant in issuer claim in this validator? Which doc are we following here?

issuers.add(STS_WINDOWS_ISSUER + tenantId + "/");
issuers.add(STS_CHINA_CLOUD_API_ISSUER + tenantId + "/");
return issuers;
}

private Predicate<String> validIssuer(Set<String> allowedTenantIds) {
return tid -> {
if (tid.startsWith(LOGIN_MICROSOFT_ONLINE_ISSUER) || tid.startsWith(STS_WINDOWS_ISSUER) ||
tid.startsWith(STS_CHINA_CLOUD_API_ISSUER)) {
for (String allowedTenantId : allowedTenantIds) {
if (tid.contains(allowedTenantId)) {
return true;
}
}
}
return false;
};
}

/**
* {@inheritDoc}
*/
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
Assert.notNull(token, "token cannot be null");
return this.validator.validate(token);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.azure.spring.aad.resource.validator;

import com.azure.spring.autoconfigure.aad.AADTokenClaim;
import java.util.Set;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

public class AzureJwtTenantValidator implements OAuth2TokenValidator<Jwt> {

private final JwtClaimValidator<String> validator;

public AzureJwtTenantValidator(String tenantId, Set<String> allowedTenantids) {
Assert.notNull(tenantId, "tenantId cannot be null");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need both to be non-null here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes,It will be modified.

Assert.notNull(allowedTenantids, "allowedTenantids cannot be null");
if (allowedTenantids.isEmpty()) {
if (StringUtils.isEmpty(tenantId)) {
this.validator = new JwtClaimValidator(AADTokenClaim.TID, tid -> true);
} else {
this.validator = new JwtClaimValidator(AADTokenClaim.TID, tenantId::equals);
}
} else {
this.validator = new JwtClaimValidator(AADTokenClaim.TID, allowedTenantids::contains);
}
}

/**
* {@inheritDoc}
*/
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
Assert.notNull(token, "token cannot be null");
return this.validator.validate(token);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import com.azure.spring.aad.implementation.AuthorizationProperties;
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import java.util.HashSet;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
Expand Down Expand Up @@ -40,6 +42,8 @@ public class AADAuthenticationProperties {

private Map<String, AuthorizationProperties> authorization = new HashMap<>();

private Set<String> allowedTenantIds = new HashSet<>();

/**
* Default UserGroup configuration.
*/
Expand Down Expand Up @@ -420,6 +424,14 @@ public static String getTransitiveGroupRelationship() {
return GROUP_RELATIONSHIP_TRANSITIVE;
}

public Set<String> getAllowedTenantIds() {
return allowedTenantIds;
}

public void setAllowedTenantIds(Set<String> allowedTenantIds) {
this.allowedTenantIds = allowedTenantIds;
}

public boolean isAllowedGroup(String group) {
return Optional.ofNullable(getUserGroup())
.map(UserGroupProperties::getAllowedGroups)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public class AADTokenClaim {
public static final String NAME = "name";
public static final String TID = "tid";
public static final String ROLES = "roles";
public static final String AUD = "aud";
public static final String ISS = "iss";
}