Implemented Spring OAuth2 security and getting same access token when logging with the same user but from different device. When i logout from any one of these devices(revoke Token)other devices are also getting logged out. Is it a expected behavior or i am missing something ? hoping that sharing the massive code will not help much so kept the question short and simple.
The default behaviour of the DefaultTokenServices is to re-use existing tokens (based on the behaviour of the existing TokenStore implementations)
http://forum.spring.io/forum/spring-projects/security/oauth/121797-multiple-valid-oauth-access-tokens-for-same-client
If you want every device to be given different access_token then create your own AuthenticationKeyGenerator e.g. send your device id on authorization process and let your AuthenticationKeyGenerator process that device id to create access_token specific for that device.
(Please read org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator code to put the following solution into context)
DefaultAuthenticationKeyGenerator is available in spring. I just created a custom version with the same code with one extension, i.e., device_id sent from the client as a request parameter is retrieved from OAuth2Authentication as follows;
String deviceId = authentication.getOAuth2Request().getRequestParameters().get("device_id")
and then is put into the values map (used to generate the token finally). Hence the device_id becomes a part of the token resulting in a unique token per device.
Following is the full solution which is mostly the DefaultAuthenticationKeyGenerator apart from the bit explained above.
public class CustomAuthenticationKeyGenerator implements AuthenticationKeyGenerator
{
private static final String CLIENT_ID = "client_id";
private static final String SCOPE = "scope";
private static final String USERNAME = "username";
#Override
public String extractKey(OAuth2Authentication authentication) {
Map<String, String> values = new LinkedHashMap<String, String>();
OAuth2Request authorizationRequest = authentication.getOAuth2Request();
if (!authentication.isClientOnly()) {
values.put(USERNAME, authentication.getName());
}
values.put(CLIENT_ID, authorizationRequest.getClientId());
if (authorizationRequest.getScope() != null) {
values.put(SCOPE, OAuth2Utils.formatParameterList(authorizationRequest.getScope()));
}
String deviceId = authorizationRequest.getRequestParameters().get(CustomHeader.device_id.name());
if(deviceId != null && !deviceId.isEmpty()) {
values.put("device_id", deviceId);
}
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
}
catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("MD5 algorithm not available. Fatal (should be in the JDK).");
}
try {
byte[] bytes = digest.digest(values.toString().getBytes("UTF-8"));
return String.format("%032x", new BigInteger(1, bytes));
}
catch (UnsupportedEncodingException e) {
throw new IllegalStateException("UTF-8 encoding not available. Fatal (should be in the JDK).");
}
}
}
For those who are facing the same issue can work on the replied solution by MangEngkus, for precise solution you can also refer this link Spring OAuth2 Generate Access Token per request to the Token Endpoint
Related
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();
}
I am developing an app whose frontend is written using React.js and the backend REST API is written using the Spring framework. I wanted to add social logins to my website, so after days of googling and research, I understood that OAuth2 is the solution. I came to know that the frontend should handle getting the authorization token from the Resource Server(Facebook here) and my backend(java) should validate that token and connect with Facebook to get an access token. Then that access token should be stored in my database along with the user details(e.g email).
Here is my requirement, once the user clicks on the "Continue with Facebook" button, my app should create there account in my own database using details - email and Name(the signup feature). And later whenever they click on this button again, they will be logged in not sign up. The way other websites handle it.
As of now, I have the button working in my app, which brings me the authorization token from Facebook.
Can someone please guide me the path I should follow here.
Also, any special attention to some error handling I should follow.
Here's the general approach using Spring Boot as a REST API backed by Spring Data JPA and Spring Security that works for iOS and ember.js together. There's probably libraries and what not that you can use but I'm just going to outline the fundamental flow.
Your user object needs a one to one mapping to a facebook account. Best practice would involve encrypting the authToken before storing in the DB
#Entity
class FacebookAccount {
#Id
#GeneratedValue(strategy= GenerationType.AUTO)
Long id
String facebookUserId
String authToken
#OneToOne
#JoinColumn(name="user_id")
User user
}
#Entity
class User{
...
#OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
FacebookAccount facebookAccount
}
Use the facebook Javascript SDK to get a User Access Token and the User's Facebook User ID. You'll get a response back from facebook in your react app that looks like this in the successful case:
{
status: 'connected',
authResponse: {
accessToken: '...',
expiresIn:'...',
reauthorize_required_in:'...'
signedRequest:'...',
userID:'...'
}
}
Hit some login endpoint with the info received in step 2 like /login/facebook. I cannot predict how your app is structured. In my app, this code is handled by my Authentication Filter that implements GenericFilterBean. I pass a header X-Auth-Facebook with the token.
Verify the token. I'm doing this in a class that implements AuthenticationProvider within the Authentication authenticate(Authentication authentication) throws AuthenticationException method. This class will need your App's Access Token accessToken and the user's Token userAccessToken:
URIBuilder builder = URIBuilder.fromUri(String.format("%s/debug_token", "https://graph.facebook.com"))
builder.queryParam("access_token", accessToken)
builder.queryParam("input_token", userAccessToken)
URI uri = builder.build()
RestTemplate restTemplate = new RestTemplate()
JsonNode resp = null
try {
resp = restTemplate.getForObject(uri, JsonNode.class)
} catch (HttpClientErrorException e) {
throw new AuthenticationServiceException("Error requesting facebook debug_token", e)
}
Boolean isValid = resp.path("data").findValue("is_valid").asBoolean()
if (!isValid)
throw new BadCredentialsException("Token not valid")
String fbookUserId = resp.path("data").findValue("user_id").textValue()
if (!fbookUserId)
throw new AuthenticationServiceException("Unable to read user_id from facebook debug_token response")
// spring data repository that finds the FacebookAccount by facebook user id
FacebookAccount fbookAcct = facebookAccountRepository.findByFacebookUserId(fbookUserId)
if(!fbookAcct){
// create your user here
// save the facebook account as well
} else{
// update the existing users token
fbookAcct.authToken = userAccessToken
facebookAccountRepository.save(fbookAcct)
}
// finish the necessary steps in creating a valid Authentication
I, personally, then create a token that my client's use when accessing my API (rather than have them continue to pass the facebook token with all requests).
I also need more user provided information to create the user (a chosen username, agreeing to terms and conditions, etc). So my actual implementation throws an EntityNotFoundException instead of creating the user, which my clients then use to pop up a registration form that provides only the fields I cannot get from facebook. On submit of this from the client, I hit my /signup/facebook endpoint with the facebook token and what's needed to create my user. I fetch the profile from facebook and create the user (automatically logging them in the process).
Edit: If you want to use Spring 0Auth, you could follow the example for creating a Spring 2 Oauth Rest Template
#Bean
public OAuth2ProtectedResourceDetails facebook() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setId("facebook");
details.setClientId("233668646673605");
details.setClientSecret("33b17e044ee6a4fa383f46ec6e28ea1d");
details.setAccessTokenUri("https://graph.facebook.com/oauth/access_token");
details.setUserAuthorizationUri("https://www.facebook.com/dialog/oauth");
details.setTokenName("oauth_token");
details.setAuthenticationScheme(AuthenticationScheme.query);
details.setClientAuthenticationScheme(AuthenticationScheme.form);
return details;
}
#Bean
public OAuth2RestTemplate facebookRestTemplate(OAuth2ClientContext clientContext) {
OAuth2RestTemplate template = new OAuth2RestTemplate(facebook(), clientContext);
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Arrays.asList(MediaType.APPLICATION_JSON,
MediaType.valueOf("text/javascript")));
template.setMessageConverters(Arrays.<HttpMessageConverter<?>> asList(converter));
return template;
}
and then in use:
public String photos(Model model) throws Exception {
ObjectNode result = facebookRestTemplate
.getForObject("https://graph.facebook.com/me/friends", ObjectNode.class);
ArrayNode data = (ArrayNode) result.get("data");
ArrayList<String> friends = new ArrayList<String>();
for (JsonNode dataNode : data) {
friends.add(dataNode.get("name").asText());
}
model.addAttribute("friends", friends);
return "facebook";
}
I took the above request for friends from the project. it shouldn't be hard to tailor the above code I showed with debug_token to use the Spring OAuth rest template. Hope this helps :)
Authorization is required to fetch information from the Google Play Developer API.
I know how to do this with Postman, but implementing authorization is much more cumbersome (redirect url, handling redirects, and so on...)
These would be the steps when you already have setup the auth data inside the Google Developer API Console.
1.) GET https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/androidpublisher&response_type=code&access_type=offline&redirect_uri=http://www.myurl.com/oauth2callback&client_id=1234567890.apps.googleusercontent.com
2.) get code which was sent to redirect url.
3.) POST https://accounts.google.com/o/oauth2/token
with
grant_type:authorization_code
code:[the code I got before]
client_id:1234567890.apps.googleusercontent.com
client_secret:[my client secret]
4.) Invoke GET https://www.googleapis.com/androidpublisher/v2/applications/packageName/purchases/subscriptions/subscriptionId/tokens/token
with:
Scope: https://www.googleapis.com/auth/androidpublisher
and:
access_token as query parameter I got before.
Now I want to do all this programmatically. Obviously not so easy. I thought the Google API Client Libraries will help, but I don't see, how these lib can help me with my use case.
For example classes like GoogleAuthorizationCodeFlow expect a user id at the moment of the request, but I not necessarily have one at this moment, so I wonder how this API should be used in a clean way.
Is there a clean way to handle OAuth2.0 easier / programmatically with some API to access Google Play Developer API? Otherwise I must implement it manually.
After lots of headache (like always with Google APIs and services) I figured out how one can access Google Play Developer API information (like billing) by using existing APIs.
1.) Create in Developer API Console a service account (JSON) key:
2.) Download this service-account-private-key.json file (don't mistake it with the OAuth2.0 client secret file!).
3.) In Google Play Developer Console go to Settings -> Users & Permissions -> Invite New User and set as user e-mail of the new user the client_email from the downloaded file. Assign the access rights you want to give to this users via the checkboxes inside this view (for example 'View financial data').
4.) Add the proper dependency to your project (version ...-1.23.0 does not work for me):
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-androidpublisher</artifactId>
<version>v2-rev50-1.22.0</version>
</dependency>
5.) Load the service-account-private-key.json file into your application. In my case it's a webserver:
#Singleton
#Startup
public class WebserverConfiguration
{
private String serviceAccountPrivateKeyFilePath;
/** Global instance of the HTTP transport. */
public static HttpTransport HTTP_TRANSPORT;
/** Global instance of the JSON factory. */
public static JsonFactory JSON_FACTORY;
private GoogleCredential credential;
#PostConstruct
public void init()
{
assignServiceAccountFileProperty();
initGoogleCredentials();
}
public String getServiceAccountPrivateKeyFilePath()
{
return serviceAccountPrivateKeyFilePath;
}
public GoogleCredential getCredential()
{
return credential;
}
private void initGoogleCredentials()
{
try
{
newTrustedTransport();
newJsonFactory();
String serviceAccountContent = new String(Files.readAllBytes(Paths.get(getServiceAccountPrivateKeyFilePath())));
InputStream inputStream = new ByteArrayInputStream(serviceAccountContent.getBytes());
credential = GoogleCredential.fromStream(inputStream).createScoped(Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER));
}
catch (IOException | GeneralSecurityException e)
{
throw new InitializationException(e);
}
}
private void newJsonFactory()
{
JSON_FACTORY = JacksonFactory.getDefaultInstance();
}
private void assignServiceAccountFileProperty()
{
serviceAccountPrivateKeyFilePath = System.getProperty("service.account.file.path");
if (serviceAccountPrivateKeyFilePath == null)
{
throw new IllegalArgumentException("service.account.file.path UNKNOWN - configure it as VM startup parameter in Wildfly");
}
}
private static void newTrustedTransport() throws GeneralSecurityException, IOException
{
if (HTTP_TRANSPORT == null)
{
HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
}
}
}
6.) Now I am able the fetch Google Play Developer API information, e.g. reviews:
private void invokeGoogleApi() throws IOException
{
AndroidPublisher publisher = new AndroidPublisher.Builder(WebserverConfiguration.HTTP_TRANSPORT, WebserverConfiguration.JSON_FACTORY, configuration.getCredential()).setApplicationName("The name of my app on Google Play").build();
AndroidPublisher.Reviews reviews = publisher.reviews();
ReviewsListResponse reviewsListResponse = reviews.list("the.packagename.of.my.app").execute();
logger.info("review list response = " + reviewsListResponse.toPrettyString());
}
This worked.
I cannot test it yet, but I'm sure that fetching the billing information works as well:
private SubscriptionPurchase getPurchase() throws IOException
{
AndroidPublisher publisher = new AndroidPublisher.Builder(WebserverConfiguration.HTTP_TRANSPORT, WebserverConfiguration.JSON_FACTORY, configuration.getCredential()).setApplicationName("The name of my app on Google Play").build();
AndroidPublisher.Purchases purchases = publisher.purchases();
SubscriptionPurchase purchase = purchases.subscriptions().get("the.packagename.of.my.app", "subscriptionId", "billing token sent by the app").execute();
//do something or return
return purchase;
}
There are complete code samples and documentation for doing this in Java here
In the Java source code this authorizes like this
private static Credential authorizeWithServiceAccount(String serviceAccountEmail)
throws GeneralSecurityException, IOException {
log.info(String.format("Authorizing using Service Account: %s", serviceAccountEmail));
// Build service account credential.
GoogleCredential credential = new GoogleCredential.Builder()
.setTransport(HTTP_TRANSPORT)
.setJsonFactory(JSON_FACTORY)
.setServiceAccountId(serviceAccountEmail)
.setServiceAccountScopes(
Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER))
.setServiceAccountPrivateKeyFromP12File(new File(SRC_RESOURCES_KEY_P12))
.build();
return credential;
}
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.
Is it possible enable the Google Placess API WebService to allow cross-origin requests from my domain so that I can access the service directly from the browser? I'v been experimenting with the API-keys by creating a Browser API key and then adding my domain to the referers list, but to no avail. Not sure if that is what the refeferer property is for anyway.
Is this limitation by design, or am I missing something here?
Google Places API WebService is the service that I want to use. Neither the Places Autocomplete or Places Search in the Places Library are suitable for my particular requirement.
Cheers
Stian
This is a client-side based limitation, so the short answer is: no.
However there are websites and services that try to surmount this problem by using scripts (loading them on the fly).
Have a look here and here (these articles are about generic cross-domain AJAX requests)
The Places-API is also available inside the Maps-Javascript-API, you don't need to struggle with cross-origins there.
Let me say its impossible to get around .I tried using java instead the code just works for http request(i used here is for graph.facebook.com):
public class search {
private static String readAll(Reader rd) throws IOException {
StringBuilder sb = new StringBuilder();
int cp;
while ((cp = rd.read()) != -1) {
sb.append((char) cp);
}
return sb.toString();
}
public static JSONObject readJsonFromUrl(String url) throws IOException, JSONException {
InputStream is = new URL(url).openStream();
try {
BufferedReader rd = new BufferedReader(new InputStreamReader(is, Charset.forName("UTF-8")));
String jsonText = readAll(rd);
JSONObject json = new JSONObject(jsonText);
return json;
} finally {
is.close();
}
}
public static void main(String[] args) throws Exception{
// TODO Auto-generated method stub
System.getProperties().put("http.proxyHost", "172.16.0.2");
System.getProperties().put("http.proxyPort", "8080");
JSONObject json = readJsonFromUrl("http://maps.googleapis.com/maps/api/place/nearbysearch/json?key=AIzaSyBRlMLIdoTk-j4OZCucR47rVMLhMmvZVRw&type=hospital&location=12.8213125%2C80.0442&radius=500&_=1427359809583");
System.out.println(json.toString());
// System.out.println(json.get("about"));
// System.out.println("hello ");
}
}
If you replace the link with places api web search it will not work,the reason is that google does not give its services on HTTP domain,,, and my code only works on HTTP domain(not HTTPS)