Spring security Oauth2 client modify authorization request going to Oauth server - spring-security

I have use case where I need to add additional parameter(login hint)
to the authorize request. On researching I found that we can do this
by OAuth2AuthorizationRequestResolver. The problem I am facing is
that the HttpServeletRequest object present in this class is not the
instance of original HttpServletRequest initiated by client. So, I can
not fetch the query param or path param send by client and append them
to to the authorization URI as extra parameter(e.g. User name).
public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private OAuth2AuthorizationRequestResolver defaultResolver;
public CustomAuthorizationRequestResolver(ClientRegistrationRepository repo, String authorizationRequestBaseUri) {
defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repo, authorizationRequestBaseUri);
}
#Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
defaultResolver.resolve(request);
OAuth2AuthorizationRequest req = defaultResolver.resolve(request);
if (req != null) {
req = customizeAuthorizationRequest(req, request);
}
return req;
}
#Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
OAuth2AuthorizationRequest req = defaultResolver.resolve(request, clientRegistrationId);
if (req != null) {
req = customizeAuthorizationRequest(req, request);
}
return req;
}
private OAuth2AuthorizationRequest customizeAuthorizationRequest(OAuth2AuthorizationRequest req,
HttpServletRequest request) {
Map<String, Object> extraParams = new HashMap<String, Object>();
extraParams.putAll(req.getAdditionalParameters());
String userName = request.getParameter("userName");
extraParams.put("login_hint", userName);
return OAuth2AuthorizationRequest.from(req).additionalParameters(extraParams).build();
}
}
Any help would be appreciated.

Related

Error 403 when feign client makes POST/PUT request to micro-service

I've got a legacy app in java that can't use spring cloud. It uses a feign client to access a microservice throug the gateway.
Gateway and service are generated by jhipster 5.7.2 with OAuth2/OIDC option.
In my client, the a RequestInterceptor calls keycloak in order to get a token (direct access grant) and injects it int he header.
It's ok when I make a GET request, but I receive a 403 after POST or PUT request.
CORS is enabled on the gateway (but isn't used because the request isn't a cors request). I run it in dev mode.
Zuul routes seem to be ok.
I didn't change the config neither on the gateway nor on the service.
Does anybody have an idea ?
Below my feign client :
public interface SmartDocumentClient {
#RequestLine("GET /api/ebox/test")
//#Headers("Content-Type: application/json")
public ResponseEntity<HasEboxResponse> test();
#RequestLine("POST /api/ebox/test")
#Headers("Content-Type: application/json")
public ResponseEntity<HasEboxResponse> testPost(HasEboxRequest request);
#RequestLine("PUT /api/ebox/test")
#Headers("Content-Type: application/json")
public ResponseEntity<HasEboxResponse> testPut(HasEboxRequest request); }
My client config :
T client = Feign.builder()
.contract(new feign.Contract.Default()) //annotation openfeign pour éviter bug d'upload avec SpringMvc
.client(new OkHttpClient())
.encoder(new FormEncoder(new GsonEncoder())) //pour gérer le formData
.decoder(new ResponseEntityDecoder(new ResponseEntityDecoder(new CustomFileDecoder(new CustomGsonDecoder()))))
.requestInterceptor(interceptor)
.options(new Request.Options(timeout, timeout))
.target(SmartDocumentClient, url);
The interceptor :
public class GedRequestInterceptor implements RequestInterceptor {
public static final String AUTHORIZATION = "Authorization";
public static final String BEARER = "Bearer";
private String authUrl;
private String user;
private String password;
private String clientId;
private String clientSecret;
private RestTemplate restTemplate;
private CustomOAuth2ClientContext oAuth2ClientContext;
public GedRequestInterceptor(String authUrl, String user, String password, String clientId, String clientSecret) {
super();
this.authUrl = authUrl;
this.user = user;
this.password = password;
this.clientId = clientId;
this.clientSecret = clientSecret;
restTemplate = new RestTemplate();
//oAuth2ClientContext = new DefaultOAuth2ClientContext();
}
#Override
public void apply(RequestTemplate template) {
// demander un token à keycloak et le joindre à la request
Optional<String> token = getToken();
if (token.isPresent()) {
template.header(HttpHeaders.ORIGIN, "localhost");
template.header(AUTHORIZATION, String.format("%s %s", BEARER, token.get()));
}
}
private Optional<String> getToken() {
if (oAuth2ClientContext.getAccessToken() == null || oAuth2ClientContext.getAccessToken().isExpired()) {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("client_id", this.clientId);
map.add("client_secret", this.clientSecret);
map.add("grant_type", "password"); // client_credentials //password
map.add("username", this.user);
map.add("password", this.password);
oAuth2ClientContext.setAccessToken(askToken(map));
}
if (oAuth2ClientContext.getAccessToken() != null){
return Optional.ofNullable(oAuth2ClientContext.getAccessToken().getValue());
} else {
return Optional.empty();
}
}
private CustomOAuth2AccessToken askToken( MultiValueMap<String, String> map) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
ResponseEntity<CustomOAuth2AccessToken> response = restTemplate.postForEntity(
this.authUrl, request, CustomOAuth2AccessToken.class);
if (response != null && response.hasBody()) {
return response.getBody();
} else {
return null;
}
}
}
And finally the ressource :
#RestController
#RequestMapping("/api")
public class DocumentResource {
private static String TMP_FILE_PREFIX = "smartdoc_tmp";
public DocumentResource() {
}
#GetMapping("/ebox/test")
public ResponseEntity<HasEboxResponse> test() {
return ResponseEntity.ok(new HasEboxResponse());
}
#PostMapping("/ebox/test")
public ResponseEntity<HasEboxResponse> testPost(#RequestBody HasEboxRequest request) {
return ResponseEntity.ok(new HasEboxResponse());
}
#PutMapping("/ebox/test")
public ResponseEntity<HasEboxResponse> testPut(#RequestBody HasEboxRequest request) {
return ResponseEntity.ok(new HasEboxResponse());
}
}
Thanks !
The problem was in the spring security config. WebSecurity didn't allow to call urls like "[SERVICE_NAME]/api" without authentication. I added a rule to allow acces to some urls. If an access token is in the header, it will be forward to the service by zuul.
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/ext/*/api/**") // allow calls to services, redirect by zuul
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/app/**/*.{js,html}")
.antMatchers("/i18n/**")
.antMatchers("/content/**")
.antMatchers("/swagger-ui/index.html")
.antMatchers("/test/**");
}
In order to call others services by UI and let inject access token by the gateway, I defined two groups of routes in my zuul config,
routes:
myservice:
path: /myservice/**
serviceId: myservice
myservice_ext:
path: /ext/myservice/**
serviceId: myservice
/ext/myService... : referencing services and don't and ignore by spring secu
/myService... : referencing services but handled by spring secu

web api authentication from client side

i have a www.api.com and a www.client.com
all registration will be done at api.com and login will be done at api.com. client.com will only be able to see the UI of the login form.
after user login and api.com return a token to user. How to i use the token to access the rest of the webapi in the api.com? i want to access the GetExployeeByID method. after use login. i stored the token in the sessionStorage.setItem('token', data.access_token)
api method
[RoutePrefix("api/Customer")]
public class CustomerController : ApiController
{
List<customer> list = new List<customer>() { new customer {id=1 ,customerName="Marry",age=13},
new customer { id = 2, customerName = "John", age = 24 } };
[Route("GetExployeeByID/{id:long}")]
[HttpGet]
[Authorize]
public customer GetExployeeByID(long id)
{
return list.FirstOrDefault(x=>x.id==id);
}
}
update 1
this is my ajax post to call the api after login
function lgetemp() {
$.ajax({
url: 'http://www.azapi.com:81/api/customer/GetExployeeByID/1',
datatype:"json",
type: 'get',
headers: {
"access_token":sessionStorage.getItem("token")
},
crossDomain: true,
success: function (data) {
debugger
alert(data.customerName)
},
error: function (err) {
debugger
alert('error')
}
})
}
You should pass the token in the header of the request from the client to the api
Authorization Basic yJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY=
The from your API you can query the headers and pull out the token.
string authorizationHeader = HttpContext.Current.Request.Headers["Authorization"];
string toke = authorizationHeader.Replace("Bearer ", String.Empty);
What I've done on my latest project is have a class AuthToken that does a lot of this for me
public class AuthToken : IAuthToken
{
private string _raw;
private IDictionary<string, string> _deserialized;
public string Raw
{
get
{
if (String.IsNullOrWhiteSpace(_raw))
{
string authorizationHeader = HttpContext.Current.Request.Headers["Authorization"];
_raw = authorizationHeader.Replace("Bearer ", String.Empty);
}
return _raw;
}
}
public IDictionary<string, string> Deserialized
{
get
{
if (_deserialized == null)
{
string[] tokenSplit = Raw.Split('.');
string payload = tokenSplit[1];
byte[] payloadBytes = Convert.FromBase64String(payload);
string payloadDecoded = Encoding.UTF8.GetString(payloadBytes);
_deserialized = JsonConvert.DeserializeObject<IDictionary<string, string>>(payloadDecoded);
}
return _deserialized;
}
}
}
Then I inject that into a UserContext class that I can inject into my controllers etc. The user context can then pull out claims from the token as needed. (assuming its a JWT)
public class UserContext : IUserContext
{
private IList<Claim> _claims;
private string _identifier;
private string _email;
private string _clientId;
public IAuthToken Token { get; }
public IList<Claim> Claims
{
get
{
return _claims ?? (_claims = Token.Deserialized.Select(self => new Claim(self.Key, self.Value)).ToList());
}
}
public string Identifier => _identifier ?? (_identifier = Token.Deserialized.ContainsKey("sub") ? Token.Deserialized["sub"] : null);
public string Email => _email ?? (_email = Token.Deserialized.ContainsKey(ClaimTypes.Email) ? Token.Deserialized[ClaimTypes.Email] : null);
public UserContext(IAuthToken authToken)
{
Token = authToken;
}
}
You need to pass the token to the request header and make the call to the API url. Below function can be called by passing the URL and token which you have.
static string CallApi(string url, string token)
{
ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
using (var client = new HttpClient())
{
if (!string.IsNullOrWhiteSpace(token))
{
var t = JsonConvert.DeserializeObject<Token>(token);
client.DefaultRequestHeaders.Clear();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + t.access_token);
}
var response = client.GetAsync(url).Result;
return response.Content.ReadAsStringAsync().Result;
}
}
Refer- Token based authentication in Web API for a detailed explanation.

Spring WebSocket. Get access to Cookie in Config

I'm trying to configure WebSocket via Spring with STOMP, OAuth 2 and SockJS.
New spec tells us how to implement it using Interceptors.
The case is: if user is authenticated there is an Bearer Token in Native header of CONNECT request and there is no problem to set principal via Token.
But my task is to use BrowserToken for unauthorized users (which is saved in Cookies). How can i get it from the request?
I've found a solution:
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket/tracker")
.withSockJS()
.setInterceptors(httpSessionHandshakeInterceptor());
}
#Bean
public HandshakeInterceptor httpSessionHandshakeInterceptor() {
return new HandshakeInterceptor() {
#Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletServerRequest = (ServletServerHttpRequest) request;
HttpServletRequest servletRequest = servletServerRequest.getServletRequest();
Cookie token = WebUtils.getCookie(servletRequest, "key");
attributes.put("token", token.getValue());
}
return true;
}
#Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
};
}
And finally
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Map<String, Object> sessionAttributes = accessor.getSessionAttributes();
List<String> authorization = accessor.getNativeHeader("Authorization");
Principal user = ... ; // get Principal using authentication / browser token
accessor.setUser(user);
}
return message;
}
});
}

Manipulate request body with Zuul

We're using Zuul as edge server. I want to write a filter that replaces the multipart/form-data from an inbound request with an entity which has the first application/json part of that request.
So that for example the request with multiparts:
[multipart/form-data]
[Part 1] << Application/JSON (name="info")
[Part 2] << Binary (name="file")
is translated into:
[application/json]
[Contents of Part 1]
Would this be possible with Zuul filters, and what type of filter should I use?
I recently had to peek into the body to figure out how to route messages that were incoming. The code below shows how you can pull the body from the request and transform it into a JSON object. That might get you started.
public class ActivateServicePreFilter extends ZuulFilter {
#Override
public String filterType() {
return PRE_TYPE;
}
#Override
public int filterOrder() {
return 4;
}
#Override
public boolean shouldFilter() {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
return "POST".equals(request.getMethod()) && request.getRequestURI().contains("uri-string");
}
#Override
public Object run() {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
StringBuffer jb = new StringBuffer();
String line = null;
try {
BufferedReader reader = request.getReader();
while ((line = reader.readLine()) != null)
jb.append(line);
} catch (Exception e) { /*report an error*/ }
try {
JSONObject jsonObject = new JSONObject(jb.toString());
String jsonField = jsonObject.getString("jsonFieldKey");
System.out.println(jsonField);
} catch (JSONException e) {
// crash and burn
}
return null;
}

How to create custom iPrincipal in MVC 4, WinAPI

I'm experiencing a situation that I have find nowhere in other articles. I'm designing a RESTful server to be consumed by an mobile app. In this case, username and password are part of header in app call, there is no logon screen.
The following code does the job of validating user info and the controllers has security controlled.
My question is: how can I populate iPrincipal in the ApiController controllers?
I have created a filter addressed at WebApiConfig.cs
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Filtro de login
config.Filters.Add(new tbAuthorize());
The code for tbAuthorize is:
public class tbAuthorize : AuthorizeAttribute
{
protected override bool IsAuthorized(HttpActionContext actionContext)
{
string username;
string password;
if (GetUserNameAndPassword(actionContext, out username, out password))
{
if (!isUserAuthorized(username, password))
return false;
else
{
//Users = username;
return true;
}
}
else
return false;
}
private bool GetUserNameAndPassword(HttpActionContext actionContext, out string username, out string password)
{
username = "";
password = "";
if (actionContext.Request.Headers.Authorization == null) return false;
// Convert 64 code to separated string[]
string[] s = ParseAuthHeader(actionContext.Request.Headers.Authorization.ToString());
if (s == null)
return false;
username = s[0];
password = s[1];
return true;
}
private string[] ParseAuthHeader(string authHeader)
{
// Check this is a Basic Auth header
if (authHeader == null || authHeader.Length == 0 || !authHeader.StartsWith("Basic")) return null;
// Pull out the Credentials with are seperated by ':' and Base64 encoded
string base64Credentials = authHeader.Substring(6);
string[] credentials = Encoding.ASCII.GetString(Convert.FromBase64String(base64Credentials)).Split(new char[] { ':' });
if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0]) || string.IsNullOrEmpty(credentials[0])) return null;
// Okay this is the credentials
return credentials;
}
private bool isUserAuthorized(string username, string password)
{
// Valid the user at database
var userId = new UsersController().Login(username, password);
// Membership.GetUser() is null
//Users = Membership.GetUser().Email;
return userId != 0;
}
}
The issue is that I have no access to a cookie in Response and I did not find to way to populate iPrincipal.
I need to has data in this.User.Identity.Name, as this:
[tbAuthorize]
public class UsersController : ApiController
{
public void test()
{
string x = this.User.Identity.Name;
}
Thanks for any help,
Marco Castro
Authentication and Authorization are two differents things. Before authorizing a user you have to authenticate their.
With WebApi you have the concept of pipeline with Delegatinghandler. Message goes from one handler to the next until one send the response. I recommend you to create a DelegatingHandler to authentificate users. Then you can use AuthorizeAttribute to prevent unauthenticated user to access your API.
Here's an example to authenticate user with HTTP Basic
public abstract class BasicAuthMessageHandler : DelegatingHandler
{
private const string BasicAuthResponseHeader = "WWW-Authenticate";
private const string BasicAuthResponseHeaderValue = "Basic Realm=\"{0}\"";
protected BasicAuthMessageHandler()
{
}
protected BasicAuthMessageHandler(HttpConfiguration httpConfiguration)
{
InnerHandler = new HttpControllerDispatcher(httpConfiguration);
}
protected virtual string GetRealm(HttpRequestMessage message)
{
return message.RequestUri.Host;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Process request
AuthenticationHeaderValue authValue = request.Headers.Authorization;
if (authValue != null && !String.IsNullOrWhiteSpace(authValue.Parameter) &&
string.Equals(authValue.Scheme, "basic", StringComparison.OrdinalIgnoreCase))
{
// Try to authenticate user
IPrincipal principal = ValidateHeader(authValue.Parameter);
if (principal != null)
{
request.GetRequestContext().Principal = principal;
}
}
return base.SendAsync(request, cancellationToken) // Send message to the InnerHandler
.ContinueWith(task =>
{
// Process response
var response = task.Result;
if (response.StatusCode == HttpStatusCode.Unauthorized &&
!response.Headers.Contains(BasicAuthResponseHeader))
{
response.Headers.Add(BasicAuthResponseHeader,
string.Format(BasicAuthResponseHeaderValue, GetRealm(request)));
}
return response;
}, cancellationToken);
}
private IPrincipal ValidateHeader(string authHeader)
{
// Decode the authentication header & split it
var fromBase64String = Convert.FromBase64String(authHeader);
var lp = Encoding.Default.GetString(fromBase64String);
if (string.IsNullOrWhiteSpace(lp))
return null;
string login;
string password;
int pos = lp.IndexOf(':');
if (pos < 0)
{
login = lp;
password = string.Empty;
}
else
{
login = lp.Substring(0, pos).Trim();
password = lp.Substring(pos + 1).Trim();
}
return ValidateUser(login, password);
}
protected abstract IPrincipal ValidateUser(string userName, string password);
}
Write you own user validation logic. For example:
public class SampleBasicAuthMessageHandler : BasicAuthMessageHandler
{
protected override IPrincipal ValidateUser(string userName, string password)
{
if (string.Equals(userName, "Meziantou", StringComparison.OrdinalIgnoreCase) && password == "123456")
return new GenericPrincipal(new GenericIdentity(userName, "Basic"), new string[0]);
return null;
}
}
Finally you have to register the Handler
HttpConfiguration config = new HttpConfiguration();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.MessageHandlers.Add(new SampleBasicAuthMessageHandler());
You'll find a complete example on Github: https://github.com/meziantou/Samples/tree/master/Web%20Api%20-%20Basic%20Authentication

Resources