Oauth2 Spring Security Resource Server and Independent Auth Server - spring

everyone!
I'm new to Oauth2 and I've had different approaches with it.
I have a doubt. I'm actually building a Provider Server with Spring Security and I have an external access token provider (Google and AWS Cognito).
I know the process to get the access token following the code grant flow (Which is the one I want to implement). I built an Android app that gets the code and changes it for the access token.
My question is:
How do I validate that the token I'm sending to the Provider Server is a valid one using Spring Security to also access the protected resources that the server has?
Thank you in advance.

I think there are two alternatives either u get the public key and verify the token urself or maybe they have an endpoint where you can send the token and know if its a valid one or not.
Something like this
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
// Specify the CLIENT_ID of the app that accesses the backend:
.setAudience(Collections.singletonList(CLIENT_ID))
// Or, if multiple clients access the backend:
//.setAudience(Arrays.asList(CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3))
.build();
// (Receive idTokenString by HTTPS POST)
GoogleIdToken idToken = verifier.verify(idTokenString);
if (idToken != null) {
Payload payload = idToken.getPayload();
// Print user identifier
String userId = payload.getSubject();
System.out.println("User ID: " + userId);
// Get profile information from payload
String email = payload.getEmail();
boolean emailVerified = Boolean.valueOf(payload.getEmailVerified());
String name = (String) payload.get("name");
String pictureUrl = (String) payload.get("picture");
String locale = (String) payload.get("locale");
String familyName = (String) payload.get("family_name");
String givenName = (String) payload.get("given_name");
// Use or store profile information
// ...
} else {
System.out.println("Invalid ID token.");
}
Link: https://developers.google.com/identity/sign-in/web/backend-auth

Related

Login to web server with Google Sign-in

I'm connecting to a 3rd party web server from an HTTP client (Java or Dart - Android app) to download some resources (XML or IMG files) that belong to the current user on that server. This site requires login with Google Sing-In. I have everything set up in my Android app to login the user with Google, I obtained their authorization idToken. But how do actually use it in HTTP GET or POST methods to download the protected resources?
With BASIC authentication it's easy - just set HTTP 'Authorization' header correctly ("Basic " + user:password encoded as base64), call GET, and I download the desired resource. But I cannot find any information on how to do this with Google Sing-In. Do I send the idToken I received from Google in some headers? What other magic is needed?
Adding a Java code snippet, hope it helps:
// (Receive authCode via HTTPS POST)
if (request.getHeader('X-Requested-With') == null) {
// Without the `X-Requested-With` header, this request could be forged. Aborts.
}
// Set path to the Web application client_secret_*.json file you downloaded from the
// Google API Console: https://console.developers.google.com/apis/credentials
// You can also find your Web application client ID and client secret from the
// console and specify them directly when you create the GoogleAuthorizationCodeTokenRequest
// object.
String CLIENT_SECRET_FILE = "/path/to/client_secret.json";
// Exchange auth code for access token
GoogleClientSecrets clientSecrets =
GoogleClientSecrets.load(
JacksonFactory.getDefaultInstance(), new FileReader(CLIENT_SECRET_FILE));
GoogleTokenResponse tokenResponse =
new GoogleAuthorizationCodeTokenRequest(
new NetHttpTransport(),
JacksonFactory.getDefaultInstance(),
"https://www.googleapis.com/oauth2/v4/token",
clientSecrets.getDetails().getClientId(),
clientSecrets.getDetails().getClientSecret(),
authCode,
REDIRECT_URI) // Specify the same redirect URI that you use with your web
// app. If you don't have a web version of your app, you can
// specify an empty string.
.execute();
String accessToken = tokenResponse.getAccessToken();
// Use access token to call API
GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
Drive drive =
new Drive.Builder(new NetHttpTransport(), JacksonFactory.getDefaultInstance(), credential)
.setApplicationName("Auth Code Exchange Demo")
.build();
File file = drive.files().get("appfolder").execute();
// Get profile info from ID token
GoogleIdToken idToken = tokenResponse.parseIdToken();
GoogleIdToken.Payload payload = idToken.getPayload();
String userId = payload.getSubject(); // Use this value as a key to identify a user.
String email = payload.getEmail();
boolean emailVerified = Boolean.valueOf(payload.getEmailVerified());
String name = (String) payload.get("name");
String pictureUrl = (String) payload.get("picture");
String locale = (String) payload.get("locale");
String familyName = (String) payload.get("family_name");
String givenName = (String) payload.get("given_name");
For detailed info, find all the required steps and references at: https://developers.google.com/identity/sign-in/web/server-side-flow#step_1_create_a_client_id_and_client_secret

How to flow user Consent for a Web API to access MS Graph user profile in AAD V2 end point with MSAL library

I'm trying to build a feature where a client application retrieves the graph resources via WebAPI layer. The scenario has following applications:
Angular5 Client application
ASP.Net Core Web API
The Angular5 client application uses MSAL to authenticate against application (resisted as Converged application via apps.dev.microsoft.com registration application; AAD v2 endpoint).
The authentication flow defines the Web API as scope while login or getting access token
constructor() {
var logger = new Msal.Logger((logLevel, message, piiEnabled) =>
{
console.log(message);
},
{ level: Msal.LogLevel.Verbose, correlationId: '12345' });
this.app = new Msal.UserAgentApplication(
CONFIGSETTINGS.clientId,
null,
this.authCallback,
{
redirectUri: window.location.origin,
cacheLocation: 'localStorage',
logger: logger
}
);
}
public getAPIAccessToken() {
return this.app.acquireTokenSilent(CONFIGSETTINGS.scopes).then(
accessToken => {
return accessToken;
},
error => {
return this.app.acquireTokenSilent(CONFIGSETTINGS.scopes).then(
accessToken => {
return accessToken;
},
err => {
console.error(err);
}
);
}
);
}
Here scope is defined as scopes: ['api://<<guid of application>>/readAccess']. This is the exact value which was generated when I've registered the Web API in registeration portal. Also, the client application id is added as Pre-authorized applications .
The Web API layer (built in dotnet core -- and uses JwtBearer to validate the authentication), defines the API which internally fetches the graph resources (using HttpClient). To get the access token, I've used following code
public async Task<string> GetAccesToken(string resourceName)
{
var userAssertion = this.GetUserAssertion();
string upn = GetLoggedInUpn();
var userTokenCache = new SessionTokenCache(upn, new Microsoft.Extensions.Caching.Memory.MemoryCache(new MemoryCacheOptions())).GetCacheInstance();
string msGraphScope = "https://graph.microsoft.com/User.Read";
string authority = string.Format("https://login.microsoftonline.com/{0}/v2.0", this.authConfig.TenantId);
ConfidentialClientApplication clientApplication = new ConfidentialClientApplication(this.authConfig.ClientId, authority, new ClientCredential(this.authConfig.AppKey), userTokenCache, null);
var result = await clientApplication.AcquireTokenOnBehalfOfAsync(new string[] { msGraphScope }, userAssertion);
return result != null ? result.AccessToken : null;
}
private UserAssertion GetUserAssertion()
{
string token = this.httpContextAccessor.HttpContext.Request.Headers["Authorization"];
string upn = GetLoggedInUpn();
if (token.StartsWith("Bearer", true, CultureInfo.InvariantCulture))
{
token = token.Trim().Substring("Bearer".Length).Trim();
return new UserAssertion(token, "urn:ietf:params:oauth:grant-type:jwt-bearer");
}
else
{
throw new Exception($"ApiAuthService.GetUserAssertion() failed: Invalid Authorization token");
}
}
Note here, The method AcquireTokenOnBehalfOfAsync is used to get the access token using graph scope. However it throws the following exception:
AADSTS65001: The user or administrator has not consented to use the application with ID '<>' named '<>'. Send an interactive authorization request for this user and resource.
I'm not sure why the of-behalf flow for AAD v2 is not working even when client application uses the Web API as scope while fetching access token and Web API registers the client application as the pre-authorized application.
Note - I've tried using the other methods of ConfidentialClientApplication but even those did not work.
Can someone please point out how the above flow can work without providing the admin consent on Web API?
I've been trying to figure this out for weeks! My solution isn't great (it requires the user to go through the consent process again for the Web API), but I'm not sure that's entirely unexpected. After all, either the Admin has to give consent for the Web API to access the graph for the user, or the user has to give consent.
Anyway, the key was getting consent from the user, which of course the Web API can't do since it has no UI. However, ConfidentialClientApplication will tell you the URL that the user has to visit with GetAuthorizationRequestUrlAsync.
Here's a snippet of the code that I used to get it working (I'm leaving out all the details of propagating the url back to the webapp, but you can check out https://github.com/rlittletht/msal-s2s-ref for a working example.)
async Task<string> GetAuthenticationUrlForConsent(ConfidentialClientApplication cca, string []graphScopes)
{
// if this throws, just let it throw
Uri uri = await cca.GetAuthorizationRequestUrlAsync(graphScopes, "", null);
return uri.AbsoluteUri;
}
async Task<string> GetAccessTokenForGraph()
{
// (be sure to use the redirectUri here that matches the Web platform
// that you added to your WebApi
ConfidentialClientApplication cca =
new ConfidentialClientApplication(Startup.clientId,
"http://localhost/webapisvc/auth.aspx",
new ClientCredential(Startup.appKey), null, null);
string[] graphScopes = {"https://graph.microsoft.com/.default"};
UserAssertion userAssertion = GetUserAssertion();
AuthenticationResult authResult = null;
try
{
authResult = await cca.AcquireTokenOnBehalfOfAsync(graphScopes, userAssertion);
}
catch (Exception exc)
{
if (exc is Microsoft.Identity.Client.MsalUiRequiredException
|| exc.InnerException is Microsoft.Identity.Client.MsalUiRequiredException)
{
// We failed because we don't have consent from the user -- even
// though they consented for the WebApp application to access
// the graph, they also need to consent to this WebApi to grant permission
string sUrl = await GetAuthenticationUrlForConsent(cca, graphScopes);
// you will need to implement this exception and handle it in the callers
throw new WebApiExceptionNeedConsent(sUrl, "WebApi does not have consent from the user to access the graph on behalf of the user", exc);
}
// otherwise, just rethrow
throw;
}
return authResult.AccessToken;
}
One of the things that I don't like about my solution is that it requires that I add a "Web" platform to my WebApi for the sole purpose of being able to give it a redirectUri when I create the ConfidentialClientApplication. I wish there was some way to just launch the consent workflow, get the user consent, and then just terminate the flow (since I don't need a token to be returned to me -- all I want is consent to be granted).
But, I'm willing to live with the extra clunky step since it actually gets consent granted and now the API can call the graph on behalf of the user.
If someone has a better, cleaner, solution, PLEASE let us know! This was incredibly frustrating to research.

RestAssured testing, get user token

What I want to do: I want to test my endpoint using RestAssured. The key is that the endpoint is available only for users who are logged in. For logging in I'm using spring security default endpoint with custom successHandler in which I'm setting some random token, saving it to database and returning in header "User-Token". I'm not creating a session on the back end. When I want to access a secured endpoint, front-end makes a call to it, with "User-Token" header. Then I'm using the token for checking in the database. Each token is different and random. Also I don't use any spring-security things for token. Now I want to test this behavior.
Technologies: React & Redux, Spring Boot, RestAssured, JUnit, Tomcat
What's not working: First of all, I'm not really sure how to obtain the token. I mean I can force it by hand to database to some test user, but AFAIK it's a bad bad practice. I read the documentation and come across part about auth().form. But below it was mentioned that it's not the best approach as have to made to the server in order to retrieve the webpage with the login details and it's not possible - webpage is totally separated from backend. I did try the approach nevertheless but it didn't work.
#Before
public void LogInUser(){
String loginUrl = "http://localhost:8080/login";
userToken =
given().auth().form("username","password").
when().get(loginUrl).getHeader("User-Token");
System.out.println(userToken);
}
So then I thought that maybe I don't need auth() at all - I don't need session, so calling the endpoint itself with data should be enough. I checked how data is passed from front-end to back-end and did this:
Form Data: username=something&password=something
#Before
public void LogInUser(){
String loginUrl = "http://localhost:8080/login";
userToken =
given().parameter("username=oliwka&password=jakies")
.when().get(loginUrl).getHeader("User-Token");
System.out.println(userToken);
}
And while it's passing, userToken is null. It's declared as class variable not method variable and it's String.
How can I obtain token for user and test my endpoint for which I need a token?
You can use below procedure to get the access token.
Step 1 : Create a method that will accept a json string and parse the data and return the access token. below is the method. You can use your preferable json parser library.
public String getAccessToken(String jsonStr) {
JSONParser parser = new JSONParser();
Object obj = null;
try {
obj = parser.parse(jsonStr);
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
JSONObject jsonObject = (JSONObject) obj;
String accessToken = (String) jsonObject.get("access_token");
System.out.println("access_token : " + accessToken);
return accessToken;
}
Step 2 : Now call your login api with username and password like below
String loginUrl = "http://localhost:8080/login";
Response res = null;
String returnValue = "";
response = given().param("username", "yourUserName")
.param("password", "yourpassword")
.param("client_id", "If Any otherwise skip it")
.param("grant_type", "If Any otherwise skip it")
.param("clear_all", "true")
.post(loginUrl);
returnValue = response.body().asString();
String accessToken = getAccessToken(returnValue);
Please let me know if you can get your desired access token.

Openiddict guidance related to external login

I have a mobile app that talks to a backend web API (core 2.0). Presently I have the API configured to use Opendidict with Facebook integration based on the configuration listed below.
public static IServiceCollection AddAuthentication(this IServiceCollection services, AppSettings settings)
{
services.AddOpenIddict<int>(options =>
{
options.AddEntityFrameworkCoreStores<RouteManagerContext>();
options.AddMvcBinders();
options.EnableAuthorizationEndpoint("/auth/authorize");
options.EnableTokenEndpoint("/auth/token");
options.AllowAuthorizationCodeFlow();
options.AllowImplicitFlow();
options.AllowPasswordFlow();
options.AllowRefreshTokenFlow();
options.SetAccessTokenLifetime(TimeSpan.FromMinutes(1));
options.SetRefreshTokenLifetime(TimeSpan.FromMinutes(20160));
options.DisableHttpsRequirement();
options.AddEphemeralSigningKey();
});
services.AddAuthentication()
.AddFacebook(o => { o.ClientId = settings.FacebookAppID; o.ClientSecret = settings.FacebookAppSecret; })
.AddOAuthValidation();
return services;
}
The password flow works perfectly when they want to use local account. What I'm struggling with is how to return the access/refresh token after successfully authenticating with Facebook. I have the standard account controller with ExternalLogin and ExternalLoginCallback which also works perfectly as I'm able to successfully login and get the local user account it's tied to and signed in.
In my mind, the user clicks facebook login, which calls ExternalLogincallBack, which logs in the user. After that all I want to do is return the access/refresh token just like the password flow.
When I try to use the ImplicitFlow by providing the implicit flow arguments in the redirect (/auth/authorize?...) from ExternalLoginCallback, I can get the access token, but no refresh token even if I specify the offline_scope. From what I read, it seems the implicit flow doesn't support refresh so I tried code flow.
When using the CodeFlow, I can get the code token from the redirect to "/auth/authorize" but can't figure out how to call into the token endpoint from the authorize endpoint to return the access/refresh token directly to the client app. Do I just need to return the code to the client and have them make another call to post to the token endpoint to get access/refresh tokens?
This doesn't feel correct and I'm stumped. Seems like I should be able to just return the access/refresh token after I've signed in externally just like what happens with password flow. Any help would be greatly appreciated as I've been struggling with this for several days.
[HttpGet("~/auth/authorize")]
public async Task<IActionResult> Authorize(OpenIdConnectRequest request)
{
if (!User.Identity.IsAuthenticated)
{
// If the client application request promptless authentication,
// return an error indicating that the user is not logged in.
if (request.HasPrompt(OpenIdConnectConstants.Prompts.None))
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIdConnectConstants.Properties.Error] = OpenIdConnectConstants.Errors.LoginRequired,
[OpenIdConnectConstants.Properties.ErrorDescription] = "The user is not logged in."
});
// Ask OpenIddict to return a login_required error to the client application.
return Forbid(properties, OpenIdConnectServerDefaults.AuthenticationScheme);
}
return Challenge();
}
// Retrieve the profile of the logged in user.
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return BadRequest(new
{
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
private async Task<AuthenticationTicket> CreateTicketAsync(OpenIdConnectRequest request, ApplicationUser user, AuthenticationProperties properties = null)
{
// Create a new ClaimsPrincipal containing the claims that will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(principal, properties, OpenIdConnectServerDefaults.AuthenticationScheme);
if (!request.IsRefreshTokenGrantType())
{
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted to allow OpenIddict to return a refresh token.
ticket.SetScopes(new[]
{
OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess,
OpenIddictConstants.Scopes.Roles
}.Intersect(request.GetScopes()));
}
ticket.SetResources("RouteManagerAPI");
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them to a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in ticket.Principal.Claims)
{
// Never include the security stamp in the access and identity tokens, as it's a secret value.
if (claim.Type == _identityOptions.Value.ClaimsIdentity.SecurityStampClaimType)
{
continue;
}
var destinations = new List<string>
{
OpenIdConnectConstants.Destinations.AccessToken
};
// Only add the iterated claim to the id_token if the corresponding scope was granted to the client application.
// The other claims will only be added to the access_token, which is encrypted when using the default format.
if ((claim.Type == OpenIdConnectConstants.Claims.Name && ticket.HasScope(OpenIdConnectConstants.Scopes.Profile)) ||
(claim.Type == OpenIdConnectConstants.Claims.Email && ticket.HasScope(OpenIdConnectConstants.Scopes.Email)) ||
(claim.Type == OpenIdConnectConstants.Claims.Role && ticket.HasScope(OpenIddictConstants.Claims.Roles)))
{
destinations.Add(OpenIdConnectConstants.Destinations.IdentityToken);
}
claim.SetDestinations(destinations);
}
return ticket;
}
When I try to use the CodeFlow, I can get the code token but can't figure out how to call into the token endpoint from the authorize endpoint to return the access/refresh token directly to the client app. Do I just need to return the code to the client and have them make another call to post to the token endpoint to get access/refresh tokens?
That's exactly what you're supposed to do as the code flow is a 2-part process: once your mobile apps has an authorization code, it must redeem it using a simple HTTP call to the token endpoint to get an access token and a refresh token.

OAuth 2.0. No session? (stateless)

I'm going to implement OAuth 2.0 and REST API with it
to grant different permissions per users and also to scale well.
To scale well, stateless is easier because there is
NO file, database, in-memory based session with it.
Below is how I understand OAuth 2.
OAuth Server give an access token to a user.
The user's access token is stored in cookie.
When user access to REST API, user sends with the access token.
Server receives request with access token.
Server find out whether access token is valid and the user has permission to do request.
Do or reject based on user's privilege.
So I do not have to worry about session storage. Right?
What you are describing here, is the OAuth 2 Implicit Grant flow. OAuth 2 also includes three other flows, but as it seems that your ressource owner (the user) is initiating requests using browser side Javascript (you were talking about cookies), this is the flow you should go for.
On client side, OAuth only requires you to store the access_token for accessing protected ressources (and a refresh_token if you're going for an expiring access_token).
A more recent innovation is JWT - JSON Web Token.
Here is a link to the spec:
JWT - JSON Web Token
JWT is a method of using Hashed tokens using a Hashing method such as HMAC which stands for a Hash-based Message Authentication Code. Because the token is hashed using a secret key, the server can determine if the token has been tampered with.
Here is an example method to create a Hashed token for JWT:
public String createTokenForUser(User user) {
byte[] userBytes = toJSON(user);
byte[] hash = createHmac(userBytes);
final StringBuilder sb = new StringBuilder(170);
sb.append(toBase64(userBytes));
sb.append(SEPARATOR);
sb.append(toBase64(hash));
return sb.toString();
}
Here is an example of decoding a token to ensure it was not tampered with:
public User parseUserFromToken(String token) {
final String[] parts = token.split(SEPARATOR_SPLITTER);
if (parts.length == 2 && parts[0].length() > 0 && parts[1].length() > 0) {
try {
final byte[] userBytes = fromBase64(parts[0]);
final byte[] hash = fromBase64(parts[1]);
boolean validHash = Arrays.equals(createHmac(userBytes), hash);
if (validHash) {
final User user = fromJSON(userBytes);
if (new Date().getTime() < user.getExpires()) {
return user;
}
}
} catch (IllegalArgumentException e) {
//log tampering attempt here
}
}
return null;
}
Here is an article with a more complete example: Stateless Authentication

Resources