Skip to content
This repository has been archived by the owner on Jun 7, 2024. It is now read-only.

Commit

Permalink
Merge pull request #840 from zalando/aruha-602
Browse files Browse the repository at this point in the history
aruha-602: return correct problem json response
  • Loading branch information
adyach authored Feb 15, 2018
2 parents 37ef951 + 00db48e commit 9ce4242
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.web.client.RestTemplate;
Expand Down Expand Up @@ -116,6 +117,10 @@ public OAuth2Authentication loadAuthentication(final String accessToken) throws
} else {
return localService.loadAuthentication(accessToken);
}
} catch (final OAuth2Exception e) {
throw e;
} catch (final RuntimeException e) {
throw new OAuth2Exception(e.getMessage(), e);
} finally {
context.stop();
}
Expand Down
110 changes: 96 additions & 14 deletions src/main/java/org/zalando/nakadi/config/SecurityConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,27 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.error.DefaultOAuth2ExceptionRenderer;
import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler;
import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.zalando.stups.oauth2.spring.security.expression.ExtendedOAuth2WebSecurityExpressionHandler;

import javax.ws.rs.core.Response;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;

import static org.springframework.http.HttpMethod.DELETE;
import static org.springframework.http.HttpMethod.GET;
Expand Down Expand Up @@ -49,6 +62,14 @@ public class SecurityConfiguration extends ResourceServerConfigurerAdapter {
@Value("${nakadi.oauth2.scopes.eventStreamWrite}")
private String eventStreamWriteScope;

public static String hasScope(final String scope) {
return MessageFormat.format("#oauth2.hasScope(''{0}'')", scope);
}

public static String hasUidScopeAndAnyRealm(final String realms) {
return MessageFormat.format("#oauth2.hasUidScopeAndAnyRealm(''{0}'')", realms);
}

@Override
public void configure(final HttpSecurity http) throws Exception {
LOG.info("Authentication mode: " + settings.getAuthMode());
Expand Down Expand Up @@ -77,35 +98,96 @@ public void configure(final HttpSecurity http) throws Exception {
.antMatchers(GET, "/subscriptions/**").access(hasScope(eventStreamReadScope))
.antMatchers(GET, "/health/**").permitAll()
.anyRequest().access(hasScope(uidScope));
}
else if (settings.getAuthMode() == SecuritySettings.AuthMode.BASIC) {
} else if (settings.getAuthMode() == SecuritySettings.AuthMode.BASIC) {
http.authorizeRequests()
.antMatchers(GET, "/health/**").permitAll()
.anyRequest().access(hasScope(uidScope));
}
else if (settings.getAuthMode() == SecuritySettings.AuthMode.REALM) {
} else if (settings.getAuthMode() == SecuritySettings.AuthMode.REALM) {
http.authorizeRequests()
.antMatchers(GET, "/health/**").permitAll()
.anyRequest().access(hasUidScopeAndAnyRealm(realms));
}
else {
} else {
http.authorizeRequests()
.anyRequest().permitAll();
}
}

public static String hasScope(final String scope) {
return MessageFormat.format("#oauth2.hasScope(''{0}'')", scope);
}

public static String hasUidScopeAndAnyRealm(final String realms) {
return MessageFormat.format("#oauth2.hasUidScopeAndAnyRealm(''{0}'')", realms);
}

@Override
public void configure(final ResourceServerSecurityConfigurer resources) throws Exception {
final OAuth2AuthenticationEntryPoint oAuth2AuthenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
oAuth2AuthenticationEntryPoint.setExceptionRenderer(new ProblemOauthExceptionRenderer());
resources.authenticationEntryPoint(oAuth2AuthenticationEntryPoint);
resources.tokenServices(tokenServices);
resources.expressionHandler(new ExtendedOAuth2WebSecurityExpressionHandler());
final OAuth2AccessDeniedHandler oAuth2AccessDeniedHandler = new OAuth2AccessDeniedHandler();
oAuth2AccessDeniedHandler.setExceptionRenderer(new ProblemOauthExceptionRenderer());
resources.accessDeniedHandler(oAuth2AccessDeniedHandler);
}

private static class ProblemOauthExceptionRenderer extends DefaultOAuth2ExceptionRenderer {

ProblemOauthExceptionRenderer() {
final List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
messageConverters.add(new ProblemOauthMessageConverter());
setMessageConverters(messageConverters);
}
}

private static class ProblemOauthMessageConverter extends MappingJackson2HttpMessageConverter {

@Override
protected void writeInternal(final Object object, final HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
super.writeInternal(toJsonResponse(object), outputMessage);
}

protected Object toJsonResponse(final Object object) {
if (object instanceof OAuth2Exception) {
final OAuth2Exception oae = (OAuth2Exception) object;
if (oae.getCause() != null) {
if (oae.getCause() instanceof AuthenticationException) {
return new ProblemResponse(Response.Status.UNAUTHORIZED, oae.getCause().getMessage());
}
return new ProblemResponse(Response.Status.INTERNAL_SERVER_ERROR, oae.getMessage());
}

return new ProblemResponse(Response.Status.fromStatusCode(oae.getHttpErrorCode()), oae.getMessage());
}

return new ProblemResponse(Response.Status.INTERNAL_SERVER_ERROR,
"Unrecognized error happened in authentication path");
}
}

private static class ProblemResponse {
private final String type;
private final String title;
private final int status;
private final String detail;

ProblemResponse(final Response.StatusType status, final String detail) {
this.type = "https://httpstatus.es/" + status.getStatusCode();
this.title = status.getReasonPhrase();
this.status = status.getStatusCode();
this.detail = detail;
}

public String getType() {
return type;
}

public String getTitle() {
return title;
}

public int getStatus() {
return status;
}

public String getDetail() {
return detail;
}
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ public void setFeature(final FeatureWrapper feature) {
}
} catch (final KeeperException.NoNodeException nne) {
LOG.debug("Feature {} was already disabled", feature.getFeature().getId());
} catch (final KeeperException.NodeExistsException nne) {
LOG.debug("Feature {} was already enabled", feature.getFeature().getId());
} catch (final Exception e) {
throw new RuntimeException("Issue occurred while accessing zk", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.junit.Test;
import org.zalando.nakadi.config.SecuritySettings;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

public class RealmModeAuthenticationTest extends AuthenticationTest {
Expand All @@ -24,19 +25,27 @@ private void checkHasOnlyAccessByRealm(final Endpoint endpoint) {

// token with wrong realm
mockMvc.perform(endpoint.withToken(TOKEN_WITH_WRONG_REALM).toRequestBuilder())
.andExpect(status().isForbidden());
.andExpect(status().isForbidden())
.andExpect(content().string("{\"type\":\"https://httpstatus.es/403\",\"title\":\"Forbidden\"," +
"\"status\":403,\"detail\":\"Insufficient realms for this resource\"}"));

// token with random scope
mockMvc.perform(endpoint.withToken(TOKEN_WITH_RANDOM_SCOPE).toRequestBuilder())
.andExpect(status().isForbidden());
.andExpect(status().isForbidden())
.andExpect(content().string("{\"type\":\"https://httpstatus.es/403\",\"title\":\"Forbidden\"," +
"\"status\":403,\"detail\":\"Insufficient realms for this resource\"}"));

// token with uid scope
mockMvc.perform(endpoint.withToken(TOKEN_WITH_UID_SCOPE).toRequestBuilder())
.andExpect(status().isForbidden());
.andExpect(status().isForbidden())
.andExpect(content().string("{\"type\":\"https://httpstatus.es/403\",\"title\":\"Forbidden\"," +
"\"status\":403,\"detail\":\"Insufficient realms for this resource\"}"));

// no token at all
mockMvc.perform(endpoint.withToken(null).toRequestBuilder())
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized())
.andExpect(content().string("{\"type\":\"https://httpstatus.es/401\",\"title\":\"Unauthorized\"," +
"\"status\":401,\"detail\":\"Full authentication is required to access this resource\"}"));
} catch (final Exception e) {
throw new AssertionError("Error occurred when calling endpoint: " + endpoint, e);
} catch (AssertionError e) {
Expand Down

0 comments on commit 9ce4242

Please sign in to comment.