My application has an API server (Spring Boot) and a client-facing server (React), the React server will make API requests on behalf of an authenticated user. For authentication, I use a CAS server provided by a third-party.
I used the Java Apereo CAS Client (version 3.6.2), and add the following API endpoints for the React app to call:
#RestController
public class CASController {
#Value("${cas.server-url-prefix}")
private String CASUrl;
#GetMapping("/login")
public String getCASUserId(HttpServletRequest request){
return request.getRemoteUser();
}
#GetMapping("/logout")
public void logout(HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession(false);
if(session != null || !request.isRequestedSessionIdValid()) {
String id = session.getId();
session.invalidate();
log.info("JSESSIONID: " + id + " is valid: " + request.isRequestedSessionIdValid());
}
try{
response.sendRedirect(CASUrl + "/logout");
}
catch (IOException | IllegalStateException e){
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getLocalizedMessage());
}
}
}
The above endpoints work if the Spring Boot app interacts with the client directly but it doesn't, it accepts requests from the React server. When the React app tries to call: localhost:8080/login, it failed because of the redirection.
A way to work around this is to call the CAS server from the React app, then send the required info (service URL & ticket issued by the CAS server) to the backend, and let the backend make the ticket validation call, if the ticket validation success, then the user is authenticated and a session id is sent back to the React app for subsequent requests.
My questions are:
Does the workaround even make sense? Authentification is split between the frontend (get a ticket from CAS server) and the backend (validate the issued ticket with CAS server and issue session). If it doesn't, what is a better solution?
For the workaround, I think I will need to use Spring Security for this instead of Java Apereo CAS Client? If that's the case, how do I do it?
I tried something like this:
Ticket validation endpoint:
#GetMapping("/login")
public String getCASUserId(#RequestParam #NotBlank String URL,
#RequestParam #NotBlank String ticket,
HttpServletRequest request){
// make the call the the CAS server for ticket validation
String userId = userService.getUserId(url, ticket);
HttpSession session = request.getSession();
session.setAttribute("userId", userId);
return userId;
}
The ticket is validated, session issued but no endpoint is actually protected since neither spring-security nor Java Apereo CAS Client is enabled.
Related
I have an ASP.NET 6.0 Web API project. I would like to add authentication and authorization to it, but it must use SSO via Azure.
We already have a SPA application that does this, it uses the Angular MSAL library to redirect the user to an SSO Login page, then returns to the SPA with an access token. The access token is then added to the header of each request to the Web API, which uses it to enforce authentication.
Now we want to share our web API with other teams within our organization, and we would like to have that login process just be another API call, rather than a web page.
Conceptually, a client would hit the /login endpoint of our API, passing in a userID and password. The web API would then get an access token from Azure, then return it as the payload of the login request. It's then up to the client to add that token to subsequent request headers.
I have done this with regular ASP.NET Identity, where all of the user and role data is stored in a SQL database, but since our organization uses SSO via Azure Active Directory, we would rather use that.
I have researched this topic online, and so far all of the examples I have seen use a separate SPA, just like we already have. But as this is a web api, not a front-end, we need to have an API method that does this instead.
Is this even possible? I know Microsoft would rather not have user credentials flow through our own web server, where a dishonest programmer might store them for later misuse. I understand that. But I'm not sure there's a way around this.
Thanks.
I believe you are looking for the Resource Owner Password (ROP) flow. You can use IdentityModel.OidcClient to implement it.
Sample code:
public class Program
{
static async Task Main()
{
// call this in your /login endpoint and return the access token to the client
var response = await RequestTokenAsync("bob", "bob");
if (!response.IsError)
{
var accessToken = response.AccessToken;
Console.WriteLine(accessToken);
}
}
static async Task<TokenResponse> RequestTokenAsync(string userName, string password)
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync(Constants.Authority);
if (disco.IsError) throw new Exception(disco.Error);
var response = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "roclient",
ClientSecret = "secret",
UserName = userName,
Password = password,
Scope = "resource1.scope1 resource2.scope1",
Parameters =
{
{ "acr_values", "tenant:custom_account_store1 foo bar quux" }
}
});
if (response.IsError) throw new Exception(response.Error);
return response;
}
}
Sample taken from IdentityServer4 repository where you can find more ROP flow client examples.
I would recommend that you don't go with this implementation and instead have all clients obtain their access tokens directly from Azure AD like you did with your Angular SPA.
I'm trying to get azure AD authentication working between a Blazor WASM app, and another API that I have running locally but on a different port. I need both applications to use the Azure login, but I only want the user to have to log in once on the Blazor app which should then pass those credentials through to the API.
I've set up app registrations for both apps in the portal, created the redirect url, exposed the API with a scope and I can successfully log into the blazor app and see my name using #context.User.Identity.Name.
When it then tries to call the API though, I get a 401 error back and it doesn't hit any breakpoints in the API (presumably because there is no authentication being passed across in the http request).
My code in the Blazor app sets up a http client with the base address set to the API:
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddHttpClient("APIClient", client => client.BaseAddress = new Uri("https://localhost:11001"))
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("APIClient"));
builder.Services.AddMsalAuthentication<RemoteAuthenticationState, CustomUserAccount>(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("api://d3152e51-9f5e-4ff7-85f2-8df5df5e2b2e/MyAPI");
//options.UserOptions.RoleClaim = "appRole";
});
await builder.Build().RunAsync();
}
In my API, I just have the Authorise attribute set on the class, and eventually will need roles in there too:
[Authorize]
[Route("api/[controller]")]
[ApiController]
public class CarController
Then, in my Blazor component, I then inject the http factory and try to make a request:
#inject IHttpClientFactory _factory
...
private async Task RetrieveCars()
{
var httpClient = _factory.CreateClient("APIClient");
HttpResponseMessage response = await httpClient.GetAsync("https://localhost:11001/api/cars");
var resp = await response.Content.ReadAsStringAsync();
cars = JsonSerializer.Deserialize<List<Car>>(resp);
}
but this returns the 401 error. I've also tried a few different variations like just injecting a http client (#inject HttpClient Http) but nothing seems to be adding my authorisation into the API calls. The options.UserOptions.RoleClaim is also commented out in the AddMsalAuthentication section as I wasn't sure if it was needed, but it doesn't work with or without it in there.
Can anyone explain what I'm doing wrong and what code I should be using?
Common causes.
Most cases ,we tend to forget to grant consent after giving API
permissions in the app registration portal,after exposing the api
which may lead to unauthorized error.
Other thing is when Audience doesn’t match the “aud” claim when we
track the token in jwt.io .Make sure ,Audience=clientId is configured
in the code in authentication scheme or Token validation parameters
by giving ValidAudiences.And also try with and without api:// prefix
in client id parameter.
Sometimes aud claim doesn’t match as we mistakenly send ID token
instead of Access tokens as access tokens are meant to call APIs .So
make sure you check mark both ID Token and access token in portal
while app registration.
While Enabling the authentication by injecting the [Authorize]
attribute to the Razor pages.Also add reference
Microsoft.AspNetCore.Authorization as(#using
Microsoft.AspNetCore.Authorization)
Please see the note in MS docs and some common-errors
If above are not the cases, please provide with additional error details and startup configurations or any link that you are following to investigate further.
I am currently using ABP and ocelot to build a api gateway for microservices. Everything works fine but now I want to use api gateway as a domain resolver. ABP's Domain Resolver works to resolve tenantid from different domains but i want ocelot to able to send the resolved tenant id to the downstream services(Include __tenant header in the headers of the request to the downstream service).
You can create a Middleware for adding ResolvedId to the request headers. So the ocelot will send the edited request to downstream with the tenantId.
Example:
public class MyMiddleware : IMiddleware, ITransientDependency
{
private readonly ICurrentTenant CurrentTenant;
public MyMiddleware(ICurrentTenant currentTenant)
{
CurrentTenant = currentTenant;
}
public Task InvokeAsync(HttpContext context, RequestDelegate next)
{
context.Request.Headers.Add("__tenant", CurrentTenant.Id.ToString());
return next(context);
}
}
As you see, the middleware manupilates the request, it's adding the CurrentTenantId to the request.
The CurrentTenant is found before by your "domain tenant resolver".
You can use this middleware between app.UseMultiTenancy() & app.UseOcelot()
...
app.UseMultiTenancy();
app.UseMiddleware<MyMiddleware>(); // it must be here, between them
app.UseOcelot();
...
I want to use GAE Open-ID Federated Authentication using Java Script (without Java Servlet) End Point.
Is it supported via Java Script (without Java Servlet) End Point?
If yes how could I use Open-ID Federated Authentication with Java Script end point API call?
I had tried User in endpoint API
#ApiMethod(
name = "signMe.signGoogleId",
httpMethod = "POST",
scopes = { "https://www.googleapis.com/auth/userinfo.profile" , "https://www.googleapis.com/auth/userinfo.email" }
)
public SignIn signInOpenId(User user)throws
OAuthRequestException, IOException
{
User is null always even after logging with Google\Yahoo User.
I had tried HttpServletRequest req in endpoint API.
#ApiMethod(
name = "SignMe.signOpenId",
httpMethod = HttpMethod.GET,
scopes = { "https://www.googleapis.com/auth/userinfo.profile" , "https://www.googleapis.com/auth/userinfo.email" }
)
public SignIn signInOpenId(HttpServletRequest req)
throws IOException
{
UserService userService = UserServiceFactory.getUserService();
User newUser = userService.getCurrentUser();
newUser is null always even after logging with Google\Yahoo User.
Thanks,
Deepak
You could always dump the cloud endpoints way of doing it and go with general API authentication.
https://developers.google.com/appengine/articles/openid#fa
As would be done for non Cloud Endpoints applications. The down side is you wouldn't get any client authorization abilities, so any client code hit your API.
Google Cloud Endpoints OAuth does not seem to support Open ID at this time.
I set up session handling on a google app project. This is supposed to allow my users to login and maintain state across pages. I just dumped it into the default greeting service to try it out:
public class GreetingServiceImpl extends RemoteServiceServlet implements GreetingService {
public void sessionTest(String username) {
HttpSession session = getThreadLocalRequest().getSession(false);
session.setAttribute("username", username);
}
}
then attempting to pull it out in my landing project.jsp page:
<%
String username = null;
HttpSession mysession = request.getSession(false);
if (mysession.getAttribute("username") != null) {
username = (String)mysession.getAttribute("username");
}
else {
username = "(not logged in yet)";
}
<p>You are:
<%= username %>
</p>
%>
It works, but I don't know how to send the data in sessionTest() securely. If I were sending the user's password in there too, it would be in the clear.
This would be ok (I think) if I was using https, but google app engine does not allow you to use https under custom domains (like www.mysite.com), they have to be under the (mysite.appspot.com) domain.
I'm kind of stuck here - how do we send passwords securely? If I was using php, I think I could use digest authentication (I'm not too experienced here) - can we do something like that with gwt + gae?
Thanks
Session data is stored on the server, not on the client - only an opaque token is sent to the client, to identify the client's session.
That said, you probably shouldn't store the user's password in the session - why would you want to? - or, indeed, in the clear at all.