I have Quarkus & Swagger UI setup to use my keycloak server (OIDC)
quarkus.smallrye-openapi.security-scheme=oidc
quarkus.smallrye-openapi.security-scheme-name=Keycloak
quarkus.smallrye-openapi.oidc-open-id-connect-url=https://someurl.com/auth/realms/arealm/.well-known/openid-configuration
quarkus.swagger-ui.oauth2-redirect-url=http://localhost:8080/q/swagger-ui/oauth2-redirect.html
I also have an endpoint setup to accept authorization
#Path("/health")
#Authenticated
public class ReactiveHealthResource {
#Inject
SecurityIdentity identity;
#GET
#Produces(MediaType.TEXT_PLAIN)
#RolesAllowed("admin")
public String hello() {
return "Hello RESTEasy Reactive";
}
}
In Swagger UI I can see the Authorize button and I can click it, fill in the authorization_code client details and authenticate via keycloak and get redirected back to Swagger UI.
When I then click to try an endpoint, I get a 401 error and the Bearer token header for the OIDC auth isn't sent with the request.
Have I missed a step or a setting?
#Path("/health")
#SecurityRequirement(name = "Keycloak")
#Authenticated
public class ReactiveHealthResource {
Adding the #SecurityRequirement informs Swagger UI of the authentication to use
Alex I had the same problem as you and I managed to solve it as follows.
#Path("/health")
#Authenticated
#SecuritySchemes(value = {
#SecurityScheme(securitySchemeName = "openIdConnectUrl",
type = SecuritySchemeType.OPENIDCONNECT,
openIdConnectUrl = "https://someurl.com/auth/realms/arealm/.well-known/openid-configuration")}
)
public class ReactiveHealthResource {
#Inject
SecurityIdentity identity;
#GET
#Produces(MediaType.TEXT_PLAIN)
#RolesAllowed("admin")
#SecurityScheme(securitySchemeName = "api_key",
type = SecuritySchemeType.APIKEY,
apiKeyName = "api_key",
in = SecuritySchemeIn.HEADER)
public String hello() {
return "Hello RESTEasy Reactive";
}
}
I also commented out the smallrye-openapi properties file settings
# quarkus.smallrye-openapi.security-scheme=oidc
# quarkus.smallrye-openapi.security-scheme-name=Keycloak
# quarkus.smallrye-openapi.oidc-open-id-connect-url=https://someurl.com/auth/realms/arealm/.well-known/openid-configuration
Related
I have a web application, hosted on our organization's web servers, which uses OpenID Connect (OIDC) in the shape of Active Directory Federated Services (ADFS) for authentication. It previously used forms authentication, but our organization is migrating all web apps to ADFS for improved security. The web app in general works as expected.
However, the app, written in MVC 5 targeting .NET Framework 4.7.2, also has a single WebAPI endpoint which is used for allowing users to subscribe, in Outlook, to an iCalendar feed exposed by the application (it's an appointment booking system). Before implementing ADFS, I made this work by using a custom AuthorizationFilterAttribute on the WebAPI endpoint which required username and password in the request header - basic authentication, in other words. It's far from ideal, but as far as I can tell it's the only kind of authentication Outlook supports.
Since implementing ADFS, though, the endpoint appears to be no longer accessible by Outlook (crucially, I'm not prompted for credentials when I add the calendar subscription). Adding it in the full Outlook app fails with no errors or feedback and doing so in the web client (an idea prompted by this article)
appears to work but again, I'm not prompted for credentials and no events ever appear in the calendar.
When I call it in a web browser I am redirected to the ADFS login page. When I call it in Postman with no Authorization header I similarly get the ADFS login page in response, but if I specify Basic Auth with the request and pass in my username and password, I get the correct response with the iCal feed.
As you can see below in my HandleUnathorized method I'm adding a WWW-Authenticate header which, as I understood it, was supposed to cause the client to prompt for login credentials when it received a 401 response, but this isn't happening.
My WebAPI controller looks like this:
namespace AppointmentBooking.Web.API.Controllers
{
public class AppointmentController : ApiController
{
[CustomBasicAuthentication] // Use a custom authentication filter for this method to support Outlook
[HttpGet]
[Route("ical/{username}")]
public HttpResponseMessage GetStaffICalendar(string username)
{
if (HttpContext.Current.User.Identity.IsAuthenticated == true)
{
string currentUsername = HttpContext.Current.User.Identity.Name;
if (username == currentUsername)
{
string calendarString = ICalendarService.GetICalendarForHost(currentUsername);
HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent(calendarString, new UTF8Encoding(false), "text/calendar");
return response;
}
return Request.CreateResponse(HttpStatusCode.Unauthorized, "You may only open your own iCalendar, not that of another user");
}
return Request.CreateResponse(HttpStatusCode.Unauthorized);
}
}
}
My custom AuthorizationFilterAttribute is defined as follows:
namespace AppointmentBooking.Web.Api.Filters
{
public class CustomBasicAuthentication : AuthorizationFilterAttribute
{
public override void OnAuthorization(HttpActionContext actionContext)
{
var authHeader = actionContext.Request.Headers.Authorization;
if (authHeader != null)
{
var authenticationToken = actionContext.Request.Headers.Authorization.Parameter;
var decodedAuthenticationToken = Encoding.UTF8.GetString(Convert.FromBase64String(authenticationToken));
var usernamePasswordArray = decodedAuthenticationToken.Split(':');
var userName = usernamePasswordArray[0];
var password = usernamePasswordArray[1];
var isValid = Membership.ValidateUser(userName, password);
if (isValid)
{
var principal = new GenericPrincipal(new GenericIdentity(userName), null);
actionContext.RequestContext.Principal = principal;
Thread.CurrentPrincipal = principal;
System.Web.HttpContext.Current.User = principal;
return;
}
}
HandleUnathorized(actionContext);
}
private static void HandleUnathorized(HttpActionContext actionContext)
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
actionContext.Response.Headers.Add("WWW-Authenticate", "Basic Scheme='Data' realm='Access your iCalendar");
}
}
}
And, in case it's helpful, the app's WebApiConfig.cs file is like this:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// WebAPI when dealing with JSON & JavaScript!
// Setup json serialization to serialize classes to camel (std. Json format)
var formatter = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
formatter.SerializerSettings.ContractResolver =
new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
}
}
}
How can I fix this endpoint so that it causes clients (particularly Outlook) to prompt for login credentials, rather than redirecting to the ADFS login page when I call it?
I have a similar requirement like this post mentioned. :REST spring security - Manually authenticating a new user and getting access token
According to the accepted answer, the codes will be like:
class RegisterController {
def springSecurityService
def tokenGenerator
def tokenStorageService
def register() {
//do stuff
springSecurityService.reauthenticate(username)
String tokenValue = tokenGenerator.generateToken()
tokenStorageService.storeToken(tokenValue, springSecurityService.principal)
redirect url: "http://example.org/?access_token=${tokenValue}"
}
}
I tried but it didn't work for me. It seems the TokenGenerator implementation class is not injected right. I understand the default implementation in grails-spring-security-rest TokenGenerator will be the JWT but wonder where should I register or config it.
Well if you want to use the "tokenGenerator" then you need to register it under the "resources.groovy" like below
// creating the bean of token generator
tokenGenerator(SecureRandomTokenGenerator)
and then inject it into your controller or service like below
class RegisterController {
def springSecurityService
def tokenGenerator
def tokenStorageService
def register() {
//do stuff
springSecurityService.reauthenticate(username)
String tokenValue = tokenGenerator.generateToken()
tokenStorageService.storeToken(tokenValue, springSecurityService.principal)
redirect url: "http://example.org/?access_token=${tokenValue}"
}
}
I have followed the same example (with slight modification) and its working as expected.
I have used the "userDetailsService" for generating user instance instead of "springSecurityService.reauthenticate(username)"
So my function looks like below.
/**
* For generating the access token for the user
*
* #param userName : Holds the username of the user
*
* #return : access token
*/
String generateAccessToken(String userName){
String tokenValue
try{
//load user details
def userDetails = userDetailsService.loadUserByUsername(userName)
//generate access token
tokenValue = tokenGenerator.generateAccessToken(userDetails).accessToken
//store access token
tokenStorageService.storeToken(tokenValue, userDetails)
} catch (Exception e){
//Exception handling code
}
return tokenValue
}
I have overridden the built in WebClient as below. Then I call it
public class HttpWebClient : WebClient
{
private Uri _responseUri;
public Uri ResponseUri
{
get { return _responseUri; }
}
protected override WebResponse GetWebResponse(WebRequest request)
{
WebResponse response = base.GetWebResponse(request);
_responseUri = response.ResponseUri;
return response;
}
}
Then I consume it like this:
using (HttpWebClient client = new HttpWebClient())
{
client.Headers[HttpRequestHeader.Authorization] = $"Bearer { _token }";
client.Headers[HttpRequestHeader.ContentType] = "application/json";
client.UploadData(_url, Encoding.UTF8.GetBytes(_data));
string queryString = client.ResponseUri.Query.Split('=').Last();
}
The response uri comes back with "https://login.microsoftonline" rather than url returned from the MVC controller with a query string, as it is authenticating first with that bearer token using AzureAd/OpenId. If i call it twice it returns the original _url but not the redirected one. If I remove AzureAd authentication it works fine. Is there a way to force the response uri to come back as what the MVC controller sets it to?
Assuming you use the 'UseOpenIdConnectAuthentication' and configuring it to use AAD authentication, you can modify the redirect uri by setting Notifications.RedirectToIdentityProvider, something like:
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = async _ =>
{
_.ProtocolMessage.RedirectUri = _.Request.Uri.ToString();
}
}
If you use something else , or maybe I didn't understand your problem - please supply more information
It should be possible to limit Google API OAuth2 requests to a specific google domain. It used to be possible by hacking on the end &hd=mydomain.com to the request. Using the new MVC auth stuff it seems no longer possible. Any ideas how?
public class AppFlowMetadata : FlowMetadata
{
private static readonly IAuthorizationCodeFlow flow =
new AppGoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId = "***.apps.googleusercontent.com",
ClientSecret = "******"
},
Scopes = new[] { DriveService.Scope.Drive },
DataStore = new FileDataStore(HttpContext.Current.Server.MapPath("~/App_Data"), true) ,
});
public override string GetUserId(Controller controller)
{
// In this sample we use the session to store the user identifiers.
// That's not the best practice, because you should have a logic to identify
// a user. You might want to use "OpenID Connect".
// You can read more about the protocol in the following link:
// https://developers.google.com/accounts/docs/OAuth2Login.
var user = controller.Session["user"];
if (user == null)
{
user = Guid.NewGuid();
controller.Session["user"] = user;
}
return user.ToString();
}
public override IAuthorizationCodeFlow Flow
{
get { return flow; }
}
}
public class AppGoogleAuthorizationCodeFlow : GoogleAuthorizationCodeFlow
{
public AppGoogleAuthorizationCodeFlow(GoogleAuthorizationCodeFlow.Initializer initializer) : base(initializer) { }
public override AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(String redirectUri)
{
var authorizeUri = new Uri(AuthorizationServerUrl).AddQuery("hd", "ourgoogledomain.com"); //is not in the request
var authUrl = new GoogleAuthorizationCodeRequestUrl(authorizeUri)
{
ClientId = ClientSecrets.ClientId,
Scope = string.Join(" ", Scopes),
RedirectUri = redirectUri,
//AccessType = "offline",
// ApprovalPrompt = "force"
};
return authUrl;
}
}
Passing a hd parameter is indeed the correct way to limit users to your domain. However, it is important that you verify that the user does actually belong to that hosted domain. I see in your answer that you figured out how to add this parameter back in to your request, so I will address the second part of this.
The issue is that the user can actually modify the requested URL in their client and remove the hd parameter! So while it's good to pass this parameter for the best UI for your users, you need to also verify that authenticated users do actually belong to that domain.
To see which hosted Google Apps for Work domain (if any) the user belongs to, you must include email in the list of scopes that you authorize. Then, do one of the following:
Option 1. Verify the ID Token.
When you exchange your code for an access token, the token endpoint will also return an ID Token in the id_token param (assuming you include an identity scope in your request such as email). If the user is part of a hosted domain, a hd claim will be present, you should check that it is present, and matches what you expect.
You can read more about ID tokens on Google's OpenID Connect docs (including some links to sample code and libraries to help you decode them). This tool can decode ID Tokens during testing.
Option 2. Call UserInfo
Once you have the OAuth Access Token, perform a GET request to https://www.googleapis.com/plus/v1/people/me/openIdConnect with the Access Token in the header. It will return a JSON dictionary of claims about the user. If the user is part of a hosted domain, a hd claim will be present, you should check that it is present, and matches what you expect.
Read more in the documentation for Google's UserInfo endpoint.
The main difference between Option 1 and Option 2 is that with the ID Token, you avoid another HTTP round-trip to the server making it faster, and less error-prone. You can try out both these options interactively using the OAuth2 Playground.
With the updated for .NET core package previous answers are no longer suitable. Fortunately in the new implementation there is a way to hook into authentication events to perform such task.
You will need a class that will handle 2 events - the one that fired before you go to Google and the one for when coming back. In first you limit which domain can be used to sign-in and in the second you ensure that the email with the right domain was in fact used for signin:
internal class GoogleAuthEvents : OAuthEvents
{
private string _domainName;
public GoogleAuthEvents(string domainName)
{
this._domainName = domainName?.ToLower() ?? throw new ArgumentNullException(nameof(domainName));
}
public override Task RedirectToAuthorizationEndpoint(OAuthRedirectToAuthorizationContext context)
{
return base.RedirectToAuthorizationEndpoint(new OAuthRedirectToAuthorizationContext(
context.HttpContext,
context.Options,
context.Properties,
$"{context.RedirectUri}&hd={_domainName}"));
}
public override Task TicketReceived(TicketReceivedContext context)
{
var emailClaim = context.Ticket.Principal.Claims.FirstOrDefault(
c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress");
if (emailClaim == null || !emailClaim.Value.ToLower().EndsWith(_domainName))
{
context.Response.StatusCode = 403; // or redirect somewhere
context.HandleResponse();
}
return base.TicketReceived(context);
}
}
and then you need to pass this "events handler" to the middleware via GoogleOptions class:
app.UseGoogleAuthentication(new GoogleOptions
{
Events = new GoogleAuthEvents(Configuration["Authentication:Google:LimitToDomain"])
})
#AMH, to do in simplest way you should create your own Google Provider, override method ApplyRedirect and append additional parameter like hd to address which will be using to redirect to a new google auth page:
public class GoogleAuthProvider : GoogleOAuth2AuthenticationProvider
{
public override void ApplyRedirect(GoogleOAuth2ApplyRedirectContext context)
{
var newRedirectUri = context.RedirectUri;
newRedirectUri += string.Format("&hd={0}", "your_domain.com");
context.Response.Redirect(newRedirectUri);
}
}
After that just link new provider to your options:
app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions()
{
ClientId = "your id",
ClientSecret = "your secret",
Provider = new GoogleAuthProvider(),
});
Having downloaded the source, I was able to see it is easy to subclass the request object, and add custom parameters:
public class GoogleDomainAuthorizationCodeRequestUrl : GoogleAuthorizationCodeRequestUrl
{
/// <summary>
/// Gets or sets the hosted domain.
/// When you want to limit authorizing users from a specific domain
/// </summary>
[Google.Apis.Util.RequestParameterAttribute("hd", Google.Apis.Util.RequestParameterType.Query)]
public string Hd { get; set; }
public GoogleDomainAuthorizationCodeRequestUrl(Uri authorizationServerUrl) : base(authorizationServerUrl)
{
}
}
public class AppGoogleAuthorizationCodeFlow : GoogleAuthorizationCodeFlow
{
public AppGoogleAuthorizationCodeFlow(GoogleAuthorizationCodeFlow.Initializer initializer) : base(initializer) { }
public override AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(String redirectUri)
{
var authUrl = new GoogleDomainAuthorizationCodeRequestUrl(new Uri(AuthorizationServerUrl))
{
Hd = "mydomain.com",
ClientId = ClientSecrets.ClientId,
Scope = string.Join(" ", Scopes),
RedirectUri = redirectUri
};
return authUrl;
}
}
I found this post when searching for a solution to specify the hosted domain with OpenID Connect integration to Google. I was able to get it working using the Google.Apis.Auth.AspNetCore package and the following code.
In Startup.cs
services.AddGoogleOpenIdConnect(options =>
{
options.ClientId = "*****";
options.ClientSecret = "*****";
options.SaveTokens = true;
options.EventsType = typeof(GoogleAuthenticationEvents);
});
services.AddTransient(provider => new GoogleAuthenticationEvents("example.com"));
Don't forget app.UseAuthentication(); in the Configure() method of Startup.cs.
Then the authentication events class
public class GoogleAuthenticationEvents : OpenIdConnectEvents
{
private readonly string _hostedDomain;
public GoogleAuthenticationEvents(string hostedDomain)
{
_hostedDomain = hostedDomain;
}
public override Task RedirectToIdentityProvider(RedirectContext context)
{
context.ProtocolMessage.Parameters.Add("hd", _hostedDomain);
return base.RedirectToIdentityProvider(context);
}
public override Task TicketReceived(TicketReceivedContext context)
{
var email = context.Principal.FindFirstValue(ClaimTypes.Email);
if (email == null || !email.ToLower().EndsWith(_hostedDomain))
{
context.Response.StatusCode = 403;
context.HandleResponse();
}
return base.TicketReceived(context);
}
}
I have a web application which sets a spring security context through a spring filter. Services are protected with spring annotations based on users roles. This works.
Asynchronous tasks are executed in JMS listeners (extend javax.jms.MessageListener). The setup of this listeners is done with Spring.
Messages are sent from the web application, at this time a user is authenticated. I need the same authentication in the JMS thread (user and roles) during message processing.
Today this is done by putting the spring authentication in the JMS ObjectMessage:
SecurityContext context = SecurityContextHolder.getContext();
Authentication auth = context.getAuthentication();
... put the auth object in jms message object
Then inside the JMS listener the authentication object is extracted and set in the context:
SecurityContext context = new SecurityContextImpl();
context.setAuthentication(auth);
SecurityContextHolder.setContext(context);
This works most of the time. But when there is a delay before the processing of a message, message will never be processed. I couldn't determine yet the cause of these messages loss, but I'm not sure the way we propagate authentication is good, even if it works in custer when the message is processed in another server.
Is this the right way to propagate a spring authentication ?
Regards,
Mickaƫl
I did not find better solution, but this one works for me just fine.
By sending of JMS Message I'am storing Authentication as Header and respectively by receiving recreating Security Context. In order to store Authentication as Header you have to serialise it as Base64:
class AuthenticationSerializer {
static String serialize(Authentication authentication) {
byte[] bytes = SerializationUtils.serialize(authentication);
return DatatypeConverter.printBase64Binary(bytes);
}
static Authentication deserialize(String authentication) {
byte[] decoded = DatatypeConverter.parseBase64Binary(authentication);
Authentication auth = (Authentication) SerializationUtils.deserialize(decoded);
return auth;
}
}
By sending just set Message header - you can create Decorator for Message Template, so that it will happen automatically. In you decorator just call such method:
private void attachAuthenticationContext(Message message){
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String serialized = AuthenticationSerializer.serialize(auth);
message.setStringProperty("authcontext", serialized);
}
Receiving gets more complicated, but it can be also done automatically. Instead of applying #EnableJMS use following Configuration:
#Configuration
class JmsBootstrapConfiguration {
#Bean(name = JmsListenerConfigUtils.JMS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)
#Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public JmsListenerAnnotationBeanPostProcessor jmsListenerAnnotationProcessor() {
return new JmsListenerPostProcessor();
}
#Bean(name = JmsListenerConfigUtils.JMS_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME)
public JmsListenerEndpointRegistry defaultJmsListenerEndpointRegistry() {
return new JmsListenerEndpointRegistry();
}
}
class JmsListenerPostProcessor extends JmsListenerAnnotationBeanPostProcessor {
#Override
protected MethodJmsListenerEndpoint createMethodJmsListenerEndpoint() {
return new ListenerEndpoint();
}
}
class ListenerEndpoint extends MethodJmsListenerEndpoint {
#Override
protected MessagingMessageListenerAdapter createMessageListenerInstance() {
return new ListenerAdapter();
}
}
class ListenerAdapter extends MessagingMessageListenerAdapter {
#Override
public void onMessage(Message jmsMessage, Session session) throws JMSException {
propagateSecurityContext(jmsMessage);
super.onMessage(jmsMessage, session);
}
private void propagateSecurityContext(Message jmsMessage) throws JMSException {
String authStr = jmsMessage.getStringProperty("authcontext");
Authentication auth = AuthenticationSerializer.deserialize(authStr);
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
I have implemented for myself a different solution, which seems easier for me.
Already I have a message converter, the standard JSON Jackson message converter, which I need to configure on the JMSTemplate and the listeners.
So I created a MessageConverter implementation which wraps around another message converter, and propagates the security context via the JMS message properties.
(In my case, the propagated context is a JWT token which I can extract from the current context and apply to the security context of the listening thread).
This way the entire responsibility for propagation of security context is elegantly implemented in a single class, and requires only a little bit of configuration.
Thanks great but I am handling this in easy way . put one util file and solved .
public class AuthenticationSerializerUtil {
public static final String AUTH_CONTEXT = "authContext";
public static String serialize(Authentication authentication) {
byte[] bytes = SerializationUtils.serialize(authentication);
return DatatypeConverter.printBase64Binary(bytes);
}
public static Authentication deserialize(String authentication) {
byte[] decoded = DatatypeConverter.parseBase64Binary(authentication);
Authentication auth = (Authentication) SerializationUtils.deserialize(decoded);
return auth;
}
/**
* taking message and return string json from message & set current context
* #param message
* #return
*/
public static String jsonAndSetContext(Message message){
LongString authContext = (LongString)message.getMessageProperties().getHeaders().get(AUTH_CONTEXT);
Authentication auth = deserialize(authContext.toString());
SecurityContextHolder.getContext().setAuthentication(auth);
byte json[] = message.getBody();
return new String(json);
}
}