websockets and authentication with identityserver4 - websocket

I am using .net core 1.1 and identityserver 4 to get tokens and validate users. The web api works fine reading the bearer token from the headers and getting the user principal claims.
Now I want to use a websocket (not SignalR) for sending notifications. I can open a ws:// channel (or wss) but token isn't sent with the headers, so in the .net core application I have no information of the user (User Claims and Identity).
How can I authenticate the user through the websocket? I did a search but couldn't find any helpful information.
Thanks

There are two main problems related to the authentication in WebSocket middleware:
Authorization should be called manually
First of all, authorization is not applied to web socket request (as it is not a controller which can be marked with Authorize attribute).
That's why in WebSocket middleware you need to call authorization by your self. This is easy to achieve by calling AuthenticateAsync extension method of the HttpContext object.
So, your middleware will be look something like this:
public class WebSocketMiddleware
{
private readonly RequestDelegate next;
public WebSocketMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context)
{
if (!context.WebSockets.IsWebSocketRequest)
{
await this.next.Invoke(context);
return;
}
AuthenticateResult authenticateResult =
await context.AuthenticateAsync(OAuthValidationDefaults.AuthenticationScheme);
....
});
}
So, using authentication results you can check if the user is authenticated or not and then access authenticated user information.
Passing bearer token to web sockets request
For Web Socket connections, the default Authorization header does not work, because the WebSockets JS API doesn't allow setting custom parameters. To work around this limitation, the access token is passed quite often in the query string.
To make authentication middleware to use it, you need to update authentication validation options. This basically can be done in your startup script like this:
services
.AddAuthentication()
.AddOAuthValidation(options =>
{
options.Events = new OAuthValidationEvents
{
// Note: for Web Socket connections, the default Authorization header does not work,
// because the WebSockets JS API doesn't allow setting custom parameters.
// To work around this limitation, the access token is retrieved from the query string.
OnRetrieveToken = context =>
{
context.Token = context.Request.Query["access_token"];
return Task.FromResult(0);
}
};
});
The following code can be used as an example to add access token to web socket url during connection initializing:
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
const wsUri = protocol + "//" + window.location.host + "/ws" + "?access_token=" + token;
this.socket = new WebSocket(wsUri);

Related

How to modify Response Cookie in GraphQL using Hot Chocolate in .Net 5

I am building a GraphQL API using Hot Chocolate(.net 5) and need to add authentication using the JWT token.
In REST API, I have used http only cookie to add the refresh token.
var cookieOption = new CookieOptions
{
HttpOnly = true,
Expires = DateTime.UtcNow.AddDays(7)
};
Response.Cookies.Append("refreshToken", <refreshToken.Token>, cookieOption);
In my login mutation, I do not have access to HttpResponse as in REST API.
Even Hot Chocolate's documentation does not have an example or instruction on how to access the Http Response.
I highly appreciate any help on this.
Thanks
You can use the IHttpContextAccessor to access the HttpContext and in turn modify the cookies.
public string Foo(string id, [Service] IHttpContextAccessor httpContextAccessor)
{
if (httpContextAccessor.HttpContext is not null)
{
httpContextAccessor.HttpContext.Response.Cookies...
}
}
https://chillicream.com/docs/hotchocolate/fetching-data/resolvers/#ihttpcontextaccessor

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.

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.

what protocol to use with ADFS when security webapi for non-browser clients

Our webapi endpoints are used for both browser based clients (angular) and non-browser based clients (restsharp) and the webapi are currently secured using passive WS-Federation as the protocol and ADFS as the STS. We currently use a rather convoluted workaround for the restsharp clients since passive WS-Federation really isn't optimal for non-browser clients so we would like to find a better way to secure our webapi endpoints for these types of clients without having to replace ADFS or add extra infrastructure.
My understanding is that OAuth2 "Resource Owner Password Credentials Grant" (grant_type=password) would support this scenario nicely but unfortunately it is currently not supported by ADFS.
So, my question is this, is there a nice way to use the one OAuth2 flow that ADFS supports, namely "Authorization Code Grant Flow" (grant_type=authorization_code) to support non-browser based clients?
If this is not possible, can I secure WebApi endpoints using WS-Trust and bearer tokens without resorting to using WCF?
It turns out it was possible to use WS-Trust to get a saml 2.0 token and a WebApi to consume it with a little help from Thinktecture IdentityModel. The following does not include claims transformation so if you need to add claims to the Principal, then a little more work is needed.
The owin startup for the webapi service needs to use the following from Thinktecture.IdentityModel.Owin:
app.UseSaml2BearerAuthentication(
audience: new Uri(ConfigurationManager.AppSettings["FederatedSecurity.Realm"]),
issuerThumbprint: ConfigurationManager.AppSettings["FederatedSecurity.Thumbprint"],
issuerName: ConfigurationManager.AppSettings["FederatedSecurity.Authority"]);
For the client to request the saml 2.0 token from ADFS
private static SecurityToken RequestSecurityToken()
{
var trustChannelFactory = new WSTrustChannelFactory(new UserNameWSTrustBinding(SecurityMode.TransportWithMessageCredential), new EndpointAddress(new Uri("https://yourAdfsServer/adfs/services/trust/13/usernamemixed"), new AddressHeader[0]))
{
TrustVersion = TrustVersion.WSTrust13,
Credentials = { UserName = { UserName = #"u$ern#me", Password = "p#ssw0rd" } }
};
var requestSecurityToken = new RequestSecurityToken
{
RequestType = RequestTypes.Issue,
KeyType = KeyTypes.Bearer,
TokenType = TokenTypes.Saml2TokenProfile11,
AppliesTo = new EndpointReference(_audience)
};
RequestSecurityTokenResponse response;
var securityToken = trustChannelFactory.CreateChannel().Issue(requestSecurityToken, out response);
return securityToken;
}
And for the client to call the service (using HttpClient but RestSharp will also work)
private static void CallService(SecurityToken token)
{
using (HttpClient client = new HttpClient())
{
client.SetBearerToken(Convert.ToBase64String(Encoding.UTF8.GetBytes(token.ToTokenXmlString())));
var httpMessage = client.GetAsync(new Uri(_restEndpoint)).Result;
}
}

WebApi authorization filter with token in json payload

I've been looking into Authorization with AspNetWebApi and information is a little sparse on the subject.
I've got the following options:
Pass API token on query string
Pass API token as header
Pass API token using Basic Auth
Pass API token onto the request payload in json.
Which is generally the recommended method?
I'm also wondering for point 4), how would I go about inspecting the json payload in the OnAuthorization method on the AuthorizationFilterAttribute to check whether the API token is correct?
If you want a truly secure option for authorization, something like OAuth is the way to go. This blog post provides a pretty thorough sample using the now obsolete WCF Web API but a lot of the code is salvageable. Or at least, go with using HTTP basic authentication as shown in this blog post. As Aliostad notes, make sure you're using HTTPS if you go the Basic authentication route so the token stays secure.
If you decide you want to roll your own (which almost always will be much less secure than either option above) then below is a code sample of what you'll need for the AuthorizationHanlder if you go HTTP header route. Be aware there's a good chance the way the UserPrinicipal is handled in Web API classes may change so this code is only good for the first preview release. You would need to wire-in the AuthorizationHandler like this:
GlobalConfiguration.Configuration.MessageHandlers.Add(new AuthenticationHandler());
Code for header token:
public class AuthenticationHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var requestAuthTokenList = GetRequestAuthTokens(request);
if (ValidAuthorization(requestAuthTokenList))
{
//TODO: implement a Prinicipal generator that works for you
var principalHelper = GlobalConfiguration.Configuration
.ServiceResolver
.GetService(typeof(IPrincipalHelper)) as IPrincipalHelper;
request.Properties[HttpPropertyKeys.UserPrincipalKey] =
principalHelper.GetPrinicipal(request);
return base.SendAsync(request, cancellationToken);
}
/*
** This will make the whole API protected by the API token.
** To only protect parts of the API then mark controllers/methods
** with the Authorize attribute and always return this:
**
** return base.SendAsync(request, cancellationToken);
*/
return Task<HttpResponseMessage>.Factory.StartNew(
() => new HttpResponseMessage(HttpStatusCode.Unauthorized)
{
Content = new StringContent("Authorization failed")
});
}
private static bool ValidAuthorization(IEnumerable<string> requestAuthTokens)
{
//TODO: get your API from config or however makes sense for you
var apiAuthorizationToken = "good token";
var authorized = requestAuthTokens.Contains(apiAuthorizationToken);
return authorized;
}
private static IEnumerable<string> GetRequestAuthTokens(HttpRequestMessage request)
{
IEnumerable<string> requestAuthTokens;
if (!request.Headers.TryGetValues("SomeHeaderApiKey", out requestAuthTokens))
{
//Initialize list to contain a single not found token:
requestAuthTokens = new[] {"No API token found"};
}
return requestAuthTokens;
}
}

Resources