spring security saml and OBSSOCookie - spring

our company is using Oracle access system for SAML single sign on. I implemented spring security with Spring Security SAML library, it worked great until I just found one issue recently.
Oracle Access System is using OBSSOCookie as identifier, but when saml response post back, I have no way to retrieve this cookie.
Have a look at this code:
#RequestMapping(value = "/callback")
public void callback(HttpServletRequest request, HttpServletResponse response)
throws IOException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SAMLCredential credential = (SAMLCredential) authentication.getCredentials();
try {
XMLHelper.nodeToString(SAMLUtil.marshallMessage(credential.getAuthenticationAssertion()));
} catch (MessageEncodingException e) {
e.printStackTrace();
}
String nameID = credential.getNameID().getValue();
List<Attribute> attributes = credential.getAttributes();
JSONObject jso = new JSONObject();
String uid;
String employeeType="";
String company_name="";
String FirstName;
String roles_entitled="";
String LastName;
String primary_role="";
jso.put("nameID", nameID);
jso.put("uid", uid);
jso.put("company_name", company_name);
jso.put("roles_entitled", roles_entitled);
jso.put("primary_role", primary_role);
jso.put("employeeType", employeeType);
jso.put("FirstName", FirstName);
jso.put("LastName", LastName);
String frontend_url = sideCarService.getFrontendNodeUrl();
String token = KeyGenerator.createUserToken(jso, 3600 * 24 * 30);
String encoded = new String(Base64.encodeBase64(jso.toString().getBytes()));
response.sendRedirect(frontend_url + "#t/" + token + "/atts/" + encoded);
}
Looking at this code, I can retrieve all the info from saml response, then generate a token, giving back to frontend cookie for use.
But I really want to get OBSSOCookie, so that I can use with other microservice to retrieve data from other applicaiton which is using same saml login solution.
I tried to user request.getHeaders(), but response is empty. No OBSSOCookie at all.
Any idea for how to obtain OBSSOCookie from spring saml library?
Thanks

Presuming the cookie is available to Spring SAML during validation of the SAML Response sent from IDP you can use the following approach.
Extend class WebSSOProfileConsumerImpl and implement method processAdditionalData which should return value of the OBSSOCookie. You can access the HTTP request and its HTTP headers/cookies through the SAMLMessageContext which is provided as a parameter.
The value you return will then be available under additionalData field in the SAMLCredential - which is indented for exactly these kinds of use-cases.

Related

OAuth2.0 authorization spring boot web application flow

i am using java library client for web application authentication, i produce authorization url using client secret and client id,also i provided a redirect url within google api console,but i don't know if it is necessary for me to create this server to receive refresh token?
i mean in production i should provide a separate server to receive the refresh token?(redirect url comes to this server)
the main problem is user should paste the produced url on browser by himself but i want to open browser authmaticly , the second one is about reciving the refresh token i am not sure about creating another server to recieve refreshcode and i can't use service accounts i am going with web flow authentication.
UserAuthorizer userAuthorizer =
UserAuthorizer.newBuilder()
.setClientId(ClientId.of(clientId, clientSecret))
.setScopes(SCOPES)
.setCallbackUri(URI.create(OAUTH2_CALLBACK_URL_CONFIGURED_AT_GOOGLE_CONSOLE))
.build();
baseUri = URI.create("http://localhost:" + simpleCallbackServer.getLocalPort());
System.out.printf(
"Paste this url in your browser:%n%s%n",
userAuthorizer.getAuthorizationUrl(loginEmailAddressHint, state, baseUri));
and this is local server to receive refresh token:
private static class SimpleCallbackServer extends ServerSocket {
private AuthorizationResponse authorizationResponse;
SimpleCallbackServer() throws IOException {
// Passes a port # of zero so that a port will be automatically allocated.
super(0);
}
/**
* Blocks until a connection is made to this server. After this method completes, the
* authorizationResponse of this server will be set, provided the request line is in the
* expected format.
*/
#Override
public Socket accept() throws IOException {
Socket socket = super.accept();
}
}
for those who struggling to get authorized using google oauth2.0 with spring boot
you cant redirect user to authorization url(which google authorization server gives to using your client id and client secret) use a controller to redirect user:
#GetMapping(value = "/redirect-user")
public ResponseEntity<Object> redirectToExternalUrl() throws URISyntaxException {
String url=gs.createUserAuthorizationUrl();
URI authorizationUrl = new URI(url);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setLocation(authorizationUrl);
return new ResponseEntity<>(httpHeaders, HttpStatus.FOUND);
}
at service layer createUserAuthorizationUrl() method is like below:
public String createUserAuthorizationUrl() {
clientId = "client-id";
clientSecret = "client-secret-code";
userAuthorizer =
UserAuthorizer.newBuilder()
.setClientId(ClientId.of(clientId, clientSecret))
.setScopes(SCOPES)
.setCallbackUri(URI.create("/oauth2callback"))
.build();
baseUri = URI.create("your-app-redirect-url-configured-at-google-console" + "your-spring-boot-server-port"); //giving redirect url
String redirectURL = userAuthorizer.getAuthorizationUrl(loginEmailAddressHint, state, baseUri).toString();
return redirectURL;
}
and let's create a controller to support the get request comming from google authorization server with an code. we are going to use that code to get access token from google.i get state and code by #RequestParam
and i also want to redirect user to my application.
#GetMapping(value = "/oauth2callback")
public ResponseEntity<Object> proceedeTOServer(#RequestParam String state,
#RequestParam String code) throws URISyntaxException {
String url="my-application-url-to-redirect-user";
URI dashboardURL = new URI(url);
HttpHeaders httpHeaders=new HttpHeaders();
httpHeaders.setLocation(dashboardURL);
gs.getCode(state,code);
return new ResponseEntity<>(httpHeaders,HttpStatus.FOUND);
}
and in getCode(code) in service layer i am going to send to code and receive the refresh token or access token:
UserCredentials userCredentials =userAuthorizer.getCredentialsFromCode(code, "your-app-redirect-url-configured-at-google-console" + "your-spring-boot-server-port");

Secure Spring Boot API without user authentication

I'm currently developing a few services with spring boot and some security concerns came in mind. All the exposed endpoints are public and do not require any user/password authentication. However, these services cannot be easily accessible by a caller other than our front-end application, since we must gather some user information through a form, in which a captcha performs a validation. Because of that, we need to ensure that the services are only invoked by this front-end application and that fake requests are denied.
Due to these requirements, i initially thought that making the endpoints accessible via https was enough. Notwithstanding, the possibility of replay attacks and spoofing still concerns me.
So, reading a few articles i came up with the following draft:
Please refer the client as the front-end application.
client and server should share a key-pair (public and private keys).
For every request, the following must be satisfied:
client creates a unique nonce (random number)
client generates a HMAC-SHA1 token with the shared private key
token = hmac('sha1', private_key, public_key + timestamp + nonce);
client must send the public_key, timestamp, nonce and token in header
upon receiving a request, the server checks if all the header params are present and then calculates the same hmac-sha1 token and compares with the received value from the client.
the nonce is then added to a cache manager, so that duplicated requisitions are discarded.
if any of header parameters are missing or if the calculated token is different from the one sent by the client, the requesition is also discarded.
Is this an appropriate approach? Are the benefits of such overhead worth?
These are the codes i currently have:
#Service
public class APIAuthenticationManager implements AuthenticationManager {
#Value("${security.http.api_key}")
private String apiKeyValue;
#Value("${security.http.api_key_header}")
private String apiKeyRequestHeader;
#Value("${security.valid_timestamp.thresold}")
private String timestampThresold;
#Value("${security.valid_timestamp.header}")
private String timestampHeader;
#Value("${security.nonce.header}")
private String nonce;
#Value("${security.token.header}")
private String tokenHeader;
#Value("${security.private_key}")
private String privateKey;
#Override
public Authentication authenticate(Authentication authentication) {
HttpServletRequest request = (HttpServletRequest) authentication.getPrincipal();
if (!apiKeyValue.equals(request.getParameter(apiKeyRequestHeader))) {
throw new BadCredentialsException("The API key was not found or not the expected value.");
}
String timestamp = request.getParameter(timestampHeader);
if(timestamp == null) {
throw new BadCredentialsException("Timestamp was not found or its value is invalid.");
}
Date requestIssueDate = Util.parseDate(timestamp, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
if(requestIssueDate == null) {
throw new BadCredentialsException("Timestamp was not found or its value is invalid.");
}
long expired = System.currentTimeMillis() - Integer.valueOf( timestampThresold );
if (requestIssueDate.getTime() > expired) {
throw new BadCredentialsException("Timestamp was not found or its value is invalid.");
}
// HMAC('SHA1', 'API_KEY', 'TOKEN GENERATED IN CLIENT');
// TOKEN GENERATED IN CLIENT = HMAC('SHA1', 'API_KEY', 'SECRET_KEY + TIMESTAMP + NONCE');
String tokenFromClient = request.getParameter(tokenHeader);
String calculatedToken = HMACSignatureUtil.calculateHMAC(privateKey, apiKeyValue + timestamp + nonce);
if(!tokenFromClient.equals(calculatedToken)) {
throw new BadCredentialsException("Invalid token.");
}
authentication.setAuthenticated(true);
return authentication;
}
This is the ConfigurerAdapter
#Autowired
private APIAuthenticationManager apiAuthenticationManager;
#Override
protected void configure(HttpSecurity http) throws Exception {
APIKeyAuthFilter filter = new APIKeyAuthFilter();
filter.setAuthenticationManager( apiAuthenticationManager );
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().addFilter(filter).authorizeRequests().anyRequest().authenticated();
http.requiresChannel()
.anyRequest(). requiresSecure();
}

Differentiate requests originating from different clients in OAuth2 framework of springboot

I have 3 different clients say mobile, web, iot. I am using grant_type = password and obtaining accessToken. I get requests GET /access/resource from all the clients. I want to process them differently based on their client ID. I know /oauth/check_token reponds with client_id but how to extract it in resource server
Use JWT, when authorization server creates token, default AccessTokenConverter implementation DefaultAccessTokenConverter's convertAccessToken method does: "response.put(this.clientIdAttribute, clientToken.getClientId());" for the token to also include client id. Above mentioned response is just a hashmap which will be converted to JWT.
When your resource server gets hit on GET /access/resource:
#RequestMapping("/access/resource")
public #ResponseBody Map<String,Object> getRes() throws IOException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
ObjectMapper objMapper = new ObjectMapper();
Map<String,Object> map = objMapper.convertValue(auth.getDetails(),Map.class);
Jwt jwt = JwtHelper.decode((String) map.get("tokenValue"));
Map<String,Object> claims = objMapper.readValue(jwt.getClaims(),Map.class);
// This is what you want
String clnt_id = (String) claims.get("client_id"); <<------- here
// your logic here based on clnt_id
// ex: if(clnt_id.equals("Specific client"){}
...
return Collections.emptyMap();;
}
OR
OAuth2Request also includes resolved client id:
Authentication auth =
SecurityContextHolder.getContext().getAuthentication();
String cliend_id = ((OAuth2Authentication) auth).getOAuth2Request().getClientId()
This option can be applied even if JWT is not used as Oauth2request is always there.
Take a look here to understand better:

Share SPRING_SECURITY_CONTEXT between two applications

I have two different Spring Boot Applications that run on localhost on different ports (8080, 8081) and different configs (application.yml). These apps use SSO with OAuth 2.0 to get authorization token from Authorization Server. I log in to my first application, get authorization and everything works great here. Now I need to share these authentication details with second Spring Boot App (on port 8081) to authorize second app in Authorization Server. Googled and found 2 aproaches: I can try to share HttpSession between two apps (but I think it's redundant) OR HttpSessionSecurityContextRepository as SecurityContextRepository which seems more convenient. The problem here is that I can't manage to do so and I'm still not sure that it's a good idea to share Security Context between 2 apps.
What I tried for now:
Share authorization token from first app via headers in GET request (custom-built in accordance with specification for requests for Authorization Server), but it didn't work - second app doesn't take in mind this token.
Share authorized cookie from first app to second, but it didn't work, too.
I can't do authorization through Authorization Server on second app because it may be not a Spring Boot App with #Controller but any other app without HTML forms, so I need to authorize on first app (with UI), get all the data which is needed to perform authorized requests and pass it to second app (third, fourth...) so they will be able to do authorized requests too.
Thanks in advance!
I presume that your authorization/resource server is external application.And you can login successfully with your first application so flow is working.You have two client application with own client_id, client_secret and etc. parameters.If these parameters are different then authorization/resource server will return different bareer token and sessionid cookie for first and second client application.Otherwise you need to authorize both of them in authorization/resource server.
I would offer when user do login to first app then in background you do login also for second application.
For automatically authorizing second application you can try to do oauth2 login flow manually for second application with own parameters when after successful first application login and send cookies to frontend which you got from oauth2 login.
For manual oauth2 login you can try below code:
private Cookie oauth2Login(String username, String password, String clientId, String clientSecret) {
try {
String oauthHost = InetAddress.getByName(OAUTH_HOST).getHostAddress();
HttpHeaders headers = new HttpHeaders();
RestTemplate restTemplate = new RestTemplate();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
// Basic Auth
String plainCreds = clientId + ":" + clientSecret;
byte[] plainCredsBytes = plainCreds.getBytes();
byte[] base64CredsBytes = org.apache.commons.net.util.Base64.encodeBase64(plainCredsBytes);
String base64Creds = new String(base64CredsBytes);
headers.add("Authorization", "Basic " + base64Creds);
// form param
map.add("username", username);
map.add("password", password);
map.add("grant_type", GRANT_TYPE);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map,
headers);
// CALLING TOKEN URL
OauthTokenRespone res = null;
try {
res = restTemplate.postForObject(OAUTH_HOST, request,
OauthTokenRespone.class);
} catch (Exception ex) {
ex.printStackTrace();
}
Optional<OauthTokenRespone> optRes = Optional.ofNullable(res);
String accessToken = optRes.orElseGet(() -> new OauthTokenRespone("", "", "", "", "", ""))
.getAccess_token();
// CALLING RESOURCE
headers.clear();
map.clear();
headers.setContentType(MediaType.APPLICATION_JSON);
map.add("access_token", accessToken);
request = new HttpEntity<MultiValueMap<String, String>>(map, headers);
Cookie oauthCookie = null;
if (accessToken.length() > 0) {
HttpEntity<String> response = restTemplate.exchange(
OAUTH_RESOURCE_URL.replace(OAUTH_HOST, oauthHost) + "?access_token=" + accessToken,
HttpMethod.POST, request, String.class);
String cookie = Optional.ofNullable(response.getHeaders().get("Set-Cookie"))
.orElseGet(() -> Arrays.asList(new String(""))).get(0);
if (cookie.length() > 0) {
String[] c = cookie.split(";")[0].split("=");
oauthCookie = new Cookie(c[0], c[1]);
oauthCookie.setHttpOnly(true);
}
}
return Optional.ofNullable(oauthCookie).orElseGet(() -> new Cookie("Ops", ""));
} catch (Throwable t) {
return new Cookie("Ops", "");
}
}
#JsonIgnoreProperties(ignoreUnknown = true)
public class OauthTokenRespone {
private String access_token;
private String token_type;
private String refresh_token;
private String expires_in;
private String scope;
private String organization;
// getter and setter
}
And call this method after first app login as follows :
Cookie oauthCookie = oauth2Login(authenticationRequest.getUsername(), authenticationRequest.getPassword(),
CLIENT_ID, CLIENT_SECRET);
After getting cookie you need change its name (for example JSESSIONID-SECOND) because same cookies will override each other and also need to change its domain path to second app domain.
response.addCookie(oauthCookie);
Last you need add cookie to response (it is HttpServletResponse reference).
Hope it helps!

Spring Social Reddit Extension - OAuth 2 Access_token retrieval. 401 Error

I am trying to create an extension for Reddit's Api. Reddit follows OAuth 2 for obtaining an access_token. I am using springs RestTemplate to make all POST requests to Reddit. I am able to successfully complete the first stage according to the documentation. The user is redirected to Reddit where he/she allows my application, Reddit then redirects me back to my application with a code. However, the second stage doesn't seem to work. I must use that code to make another post request to :
https://ssl.reddit.com/api/v1/access_token
Here is my attempt for obtaining an AccessGrant (SpringSocial wrapper for accesstoken sent back from Reddit). Spring Social requires you to extend OAuth2Template and implement the authentication process from there. In a typical spring application, a controller will use a helper to make a call to RedditOAuth2Template.exchangeForAccess and save the returned AccessGrant into a database.
According to the Reddit API Documentaiton a 401 response occurs due to a lack of client credentials via HTTP basic Auth. However, I am doing that in the createHeaders(String username, String password) method.
public class RedditOAuth2Template extends OAuth2Template {
private static final Logger LOG = LogManager.getLogger(RedditOAuth2Template.class);
private String client_id;
private String client_secret;
public RedditOAuth2Template(String clientId, String clientSecret) {
super(clientId, clientSecret, RedditPaths.OAUTH_AUTH_URL, RedditPaths.OAUTH_TOKEN_URL);
this.client_id = clientId;
this.client_secret = clientSecret;
setUseParametersForClientAuthentication(true);
}
#Override
#SuppressWarnings({"unchecked", "rawtypes"})
protected AccessGrant postForAccessGrant(String accessTokenUrl, MultiValueMap<String, String> parameters) {
HttpHeaders headers = createHeaders(client_id, client_secret);
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.set(accessTokenUrl, accessTokenUrl);
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<MultiValueMap<String, String>>(parameters, headers);
ResponseEntity<Map> responseEntity = getRestTemplate().exchange(accessTokenUrl, HttpMethod.POST, requestEntity, Map.class);
Map<String, Object> responseMap = responseEntity.getBody();
return extractAccessGrant(responseMap);
}
/*
Reddit requires client_id and client_secret be
placed via HTTP basic Auth when retrieving the access_token
*/
private HttpHeaders createHeaders(String username, String password) {
String auth = username + ":" + password;
byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes(Charset.forName("US-ASCII")));
HttpHeaders headers = new HttpHeaders();
String authHeader = "Basic " + new String(encodedAuth);
headers.set("Authorization", authHeader);
return headers;
}
private AccessGrant extractAccessGrant(Map<String, Object> result) {
String accessToken = (String) result.get("access_token");
String scope = (String) result.get("scope");
String refreshToken = (String) result.get("refresh_token");
// result.get("expires_in") may be an Integer, so cast it to Number first.
Number expiresInNumber = (Number) result.get("expires_in");
Long expiresIn = (expiresInNumber == null) ? null : expiresInNumber.longValue();
return createAccessGrant(accessToken, scope, refreshToken, expiresIn, result);
}
}
If you're getting a 401 response for that endpoint, you're doing one of a small number of things wrong, all related to sending the client ID & secret as HTTP Basic Authorization:
Not including a properly formatted Authorization header (i.e., Authorization: basic <b64 encoded credentials>)
Not properly base 64 encoding your credentials
Not including a client_id that for a valid OAuth2 client
Not including a semicolon between the client ID and secret
Not including the secret, or including the WRONG secret
You should check each stage of the Basic client auth, and log your output (or use a debugger to inspect it) at each stage to ensure you're not missing anything. You should also inspect the actual HTTP request you generate, and verify that the header is being sent (some HTTP libraries like to take liberties with headers)

Resources