Authentication error when SignalR tries to negotiate on HubConnection.StartAsync - signalr

I have a blazor server side web app that uses AAD for authentication. I have added SignalR to it according to the guidance. It starts up fine, but when I try to connect using the following code:
var hubUrl = _navigationManager.ToAbsoluteUri("messaging");
hubConnection = new HubConnectionBuilder()
.WithUrl(hubUrl)
.Build();
await hubConnection.StartAsync();
I get an exception. This the console debug trace:
[INF] Request finished HTTP/2 GET https://localhost:5001/css/insight-icons/fonts/Insight.woff2 - - - 304 - font/woff2 3.1879ms
[INF] Request starting HTTP/1.1 POST https://localhost:5001/messaging/negotiate?negotiateVersion=1 - 0
[INF] OpenIdConnect was not authenticated. Failure message: Not authenticated
[INF] Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
[INF] AuthenticationScheme: OpenIdConnect was challenged.
[INF] Request finished HTTP/1.1 POST https://localhost:5001/messaging/negotiate?negotiateVersion=1 - 0 - 302 0 - 962.8463ms
As mentioned I am logged in and authenticated using AAD.
Any suggestions as to what I am doing wrong of what is missing?

Note the token config:
hubConnection = new HubConnectionBuilder()
.WithUrl(hubUrl), options =>
{
options.AccessTokenProvider = async () =>
{
var accessTokenResult = await AccessTokenProvider.RequestAccessToken();
accessTokenResult.TryGetToken(out var accessToken);
var token = accessToken.Value;
return token;
};
})
.Build();
You should also configure the middleware one the server to add the tokens to requests going to the hub. (Not required to start the connection)
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>,ConfigureJwtBearerOptions>());
ConfigureJwtBearerOptions.cs (Change the endpoint)
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
public void PostConfigure(string name, JwtBearerOptions options)
{
var originalOnMessageReceived = options.Events.OnMessageReceived;
options.Events.OnMessageReceived = async context =>
{
await originalOnMessageReceived(context);
if (string.IsNullOrEmpty(context.Token))
{
var accessToken = context.Request.Query["access_token"];
var requestPath = context.HttpContext.Request.Path;
var endPoint = $"/chathub";
if (!string.IsNullOrEmpty(accessToken) &&
requestPath.StartsWithSegments(endPoint))
{
context.Token = accessToken;
}
}
};
}
}

Related

ASP.NET Core 6 Web API with firebase auth and react front end bearer auth fails

I'm having some issues with an ASP.NET Core 6 Web API and a react front end using firebase auth. I get a 401 every time the react app requests an authorized endpoint (but 200 with postman).
Using ASP.NET Core 6
I know the token I am using works fine because when I request with postman using the same bearer token I get a 200 response.
I have also tried to set ValidateIssuer = false & ValidateAudience = false & ValidateLifetime = false with no luck
Front end request (when the user logs in via the firebase/auth signInWithEmailAndPassword method
const testFetch = async () => {
getIdToken(auth.currentUser!).then(async (token) => {
const res = await fetch('https://localhost:51437/test/private', {
method: 'GET',
headers: {
Authentication: `Bearer ${token}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
});
const result = await res.json();
console.log(result);
});
};
I can also request non authorized endpoints from my web app and get them correctly so shouldn't be anything to do with cors
Adding JWT bearer auth scheme:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opt =>
{
opt.IncludeErrorDetails = true;
opt.Authority = $"https://securetoken.google.com/{builder.Configuration["Firebase:ID"]}";
opt.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidIssuer = $"https://securetoken.google.com/{builder.Configuration["Firebase:ID"]}",
ValidateAudience = true,
ValidAudience = builder.Configuration["Firebase:ID"],
ValidateLifetime = true
};
});
Setup for auth:
app.UseCors(x => x.AllowAnyMethod().AllowAnyHeader().SetIsOriginAllowed(origin => true).AllowCredentials());
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.Run();
Controller:
[ApiController]
[Route("[controller]")]
public class TestController : Controller
{
public IActionResult Index()
{
return Ok("Hello world");
}
[HttpGet("private")]
[Authorize]
public IActionResult Private()
{
return Ok(new
{
Message = "Hello from a private endpoint!"
});
}
}
Request logs
[00:41:14 DBG] AuthenticationScheme: Bearer was not authenticated.
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
Microsoft.AspNetCore.Authorization.DefaultAuthorizationService: Information: Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
[00:41:14 INF] Authorization failed. These requirements were not met:
DenyAnonymousAuthorizationRequirement: Requires an authenticated user.
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12]
AuthenticationScheme: Bearer was challenged.
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: AuthenticationScheme: Bearer was challenged.
[00:41:14 INF] AuthenticationScheme: Bearer was challenged.
After debugging it looks like my API seems to be removing the Authorization header from my front end app which is an expo web app (react) but not when the request is from postman.
The request is sent at-least in the network tab with the correct bearer

How do I use the Azure AD Authorization Code Flow?

I am building an ASP.NET (v4.8) Web application that will be hosted as an Azure App Service, but for now we are on localhost.
I am configured for Azure AD successfully and I am receiving an authorization code because I configured my app service to send the access token. The app registration has ONLY User.Read (delegated) permissions.
In my Startup.cs file, I've configured OpenIdConnectAuthenticationNotifications so that I am receiving the access code in AuthorizationCodeReceived. Here is the code:
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType( CookieAuthenticationDefaults.AuthenticationType );
app.UseCookieAuthentication(new CookieAuthenticationOptions());
authority = aadInstance + tenantId;
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions {
ClientId = clientId, Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
TokenResponseReceived = (tr) => { return Task.FromResult(0); },
AuthorizationCodeReceived = (code) => {
// you are here! what's next?
access_code = code.Code;
return Task.FromResult(0);
},
SecurityTokenReceived = (token) =>
{
return Task.FromResult(0);
},
AuthenticationFailed = (context) => { return System.Threading.Tasks.Task.FromResult(0); }
}
});
app.UseStageMarker(PipelineStage.Authenticate);
}
My objective is to call this graph endpoint as the current user to get their JobTitle and > Department from Azure AD. Here is the resource: https://graph.microsoft.com/v1.0/me
I was following this documentation, but it was not clear what to do with the provided access_code. Please help me understand.
Is this access_code a bearer token? can I use it directly to call the graph API?
Do I have to use it to call the /token endpoint to get a bearer token?
Do I have to use it to call the /authorize endpoint to get a bearer token?
I am making direct HTTP requests now, should I use MSAL or Graph SDK?
I think I am trying to accomplish this step:
This is the code I am currently working on, and it returns HTTP CODE 400 (Bad Request):
private void GetOtherProfileData()
{
var cId = Startup.clientId;
var tenantId = Startup.tenantId;
var scope = Startup.scope;
// scope: https%3A%2F%2Fgraph.microsoft.com%2Fuser.read
var code = Startup.access_code;
var redir = HttpUtility.UrlEncode(Startup.redirectUri);
var req_url = $#"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token?client_id={cId}&scope={scope}
&code={code}&redirect_uri={redir}&grant_type=authorization_code
&code_verifier=ThisIsntRandomButItNeedsToBe43CharactersLong";
var req = WebRequest.CreateHttp(req_url);
req.Method = "POST";
req.ContentLength = 0;
req.ContentType = "application/x-www-form-urlencoded";
var resp = req.GetResponse();
var str = resp.GetResponseStream();
var json = new StreamReader(str).ReadToEnd();
Trace.TraceInformation(json);
/// this should return bearer token and then we go call the /me endpoint...
///right?
}
Any code samples or pointers to recent documentation would be helpful.
Is this access_code a bearer token? can I use it directly to call the graph API?
No, code and Access_token are different. You will need a access_token to call Graph API.
Do I have to use it to call the /token endpoint to get a bearer token?
Yes, you'll need code to call token endpoint to get the bearer token.
Do I have to use it to call the /authorize endpoint to get a bearer token?
You will get the code after calling authorize endpoint. You need to pass grant_type=code to get the code in response.
I am making direct HTTP requests now, should I use MSAL or Graph SDK?
You'll need to call Graph API after you get the access_token. Along with the token it also needs proper dedicated and application User permissions from Azure side.
I found a good sample here: https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect
I was trying to do this without a client secret, that was a mistake.
This is how I implemented it =>
AuthorizationCodeReceived = async (context) => {
// you are here!
IConfidentialClientApplication clientApp = MsalAppBuilder.BuildConfidentialClientApplication();
AuthenticationResult result = await clientApp.AcquireTokenByAuthorizationCode(new[] { "User.Read" }, context.Code)
.WithSpaAuthorizationCode() //Request an authcode for the front end
.ExecuteAsync();
access_code = result.AccessToken;
// this is the bearer token.
},
This is what is inside the implementation of BuildConfidentialClientApplication:
clientapp = ConfidentialClientApplicationBuilder.Create(Startup.clientId)
.WithClientSecret(Startup.secret)
.WithRedirectUri(Startup.redirectUri)
.WithAuthority(new Uri(Startup.authority))
.Build();

How to get new access token by using refresh token in AspNet.Security.OpenIdConnect.Server (ASOS)

I am using React as client and Web API core for back end interaction.
For Authentication we are using Token based authentication using AspNet.Security.OpenIdConnect.Server (ASOS).
I have to implement refresh token scenario where on expiration of access token we use refresh token (returned by ASOS) to get new access Token.
I know one way to achieve by calling method on client is in AXIOS interceptor like below.
httpPromise.interceptors.response.use(undefined, err => {
const { config, response: { status } } = err;
const originalRequest = config;
if (status === 401) {
var refresh_Token = JSON.parse(window.localStorage.getItem('refreshToken'));
fetch(globalConstant.WEB_API_BASE_PATH + "authtoken,
{
method: "POST",
headers: new Headers({
'Content-Type': 'application/json',
})
},
data:{grant-type:"refresh_Token",refresh_token:"refresh Token ....."
)
....other logic to set new access token and make call again to existing
request.
}
})
I want to done it in WEB API Core side, so that in middle ware or in authentication pipeline it detects access token expiration and return new access token. The glimpse of WEB API code is like below.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
.... some code
serives.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = OAuthValidationDefaults.AuthenticationScheme;
})
serives.AddOAuthValidation()
serives.AddOpenIdConnectServer(options =>
{
options.ProviderType = typeof(AuthorizationProvider);
options.Provider = new AuthorizationProvider(new SecurityService());
options.TokenEndpointPath = "/authtoken";
options.UserinfoEndpointPath = "/userInfo";
options.AllowInsecureHttp = true;
options.ApplicationCanDisplayErrors = true;
});
..some code
}
The links i followed How to handle expired access token in asp.net core using refresh token with OpenId Connect and https://github.com/mderriey/aspnet-core-token-renewal/blob/master/src/MvcClient/Startup.cs

New JWT token has same expiry as old token

I've got JWT token generation working in my ASP.NET Core 2.0 Web API, but I'm running into an issue where subsequent new access tokens have the same expiry as previously generated ones.
For instance, I post login credentials, and return an access token. The access token works as expected on [Authorize] API endpoints. For testing purposed, I set the token to expire after 1 minute. After 1 minute, the token expires and the authenticated endpoints return a 401, as expected.
I'm handling the 401's in my client side application. The login form appears, and the user logs in again. A new token is generated and returned. The only issue is, this new token has the exact same 'ValidTo" DateTime as the initially generated token. Causing any calls after using this new token to return 401 because the token is already expired. I've confirmed that two different tokens are being checked, so it's not an issue with me passing the wrong token
First token failure (expected, as token expired):
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: Failed to validate the token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZGFtQHBytNDRiYS1...Do1NzM5NS8ifQ.t8DjvlGV7GZ3xucwu-1hlJRXA5owPdP9t7kfYiiJHyQ.
Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException: IDX10223: Lifetime validation failed. The token is expired.
ValidTo: '11/08/2017 19:23:09'
Current time: '11/08/2017 19:23:13'.
Second token failure (not expected, ValidTo same as previous token)
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler: Information: Failed to validate the token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZGFtQ...dDo1NzM5NS8ifQ.2TMPJvYnQl1Jw78M2nj40uD3qejBEciXfKC845saGNI.
Microsoft.IdentityModel.Tokens.SecurityTokenExpiredException: IDX10223: Lifetime validation failed. The token is expired.
ValidTo: '11/08/2017 19:23:09'
Current time: '11/08/2017 19:23:34'.
JWT Configuration in Startup.cs
services.Configure<JwtIssuerOptions>(options => {
options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
options.SigningCredentials = new SigningCredentials(SigningKey, SecurityAlgorithms.HmacSha256);
options.ValidFor = TimeSpan.FromMinutes(1);
});
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)],
ValidateAudience = true,
ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)],
ValidateIssuerSigningKey = true,
IssuerSigningKey = SigningKey,
RequireExpirationTime = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
};
});
Login action where Token is created:
[HttpPost]
public async Task<IActionResult> Login([FromBody]CredentialsViewModel credentials)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var identity = await GetClaimsIdentity(credentials.UserName, credentials.Password);
if (identity == null)
{
return BadRequest(Errors.AddErrorToModelState("login_failure", "Invalid username or password.", ModelState));
}
// Serialize and return the response
var response = new
{
id = identity.Claims.Single(c => c.Type == "id").Value,
auth_token = await _jwtFactory.GenerateEncodedToken(credentials.UserName, identity),
expires_in = (int)_jwtOptions.ValidFor.TotalSeconds
};
var json = JsonConvert.SerializeObject(response, _serializerSettings);
return new OkObjectResult(json);
}
JwtFactory method where token is being generated:
private readonly JwtIssuerOptions _jwtOptions;
public JwtFactory(IOptions<JwtIssuerOptions> jwtOptions)
{
_jwtOptions = jwtOptions.Value;
ThrowIfInvalidOptions(_jwtOptions);
}
public async Task<string> GenerateEncodedToken(string userName, ClaimsIdentity identity)
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, userName),
new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()),
new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64),
identity.FindFirst("rol"),
identity.FindFirst("id")
};
// Create the JWT security token and encode it.
var jwt = new JwtSecurityToken(
issuer: _jwtOptions.Issuer,
audience: _jwtOptions.Audience,
claims: claims,
notBefore: _jwtOptions.NotBefore,
expires: _jwtOptions.Expiration,
signingCredentials: _jwtOptions.SigningCredentials);
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
return encodedJwt;
}
The issue was in my jwtFactory, I was dependency injecting IOptions. Since this is defined in the startup, and has several properties that are automatically filled when the object is created (such as IssuedAt, which gets DateTime.NowUtc), IOptions was only returning the configuration of the first time it was loaded.
I was able to solve this by injecting IOptionsSnapshot, which grabs a new version of JwtIssuerOptions, which would have an updated IssuedAt property.
private readonly JwtIssuerOptions _jwtOptions;
public JwtFactory(IOptionsSnapshot<JwtIssuerOptions> jwtOptions)
{
_jwtOptions = jwtOptions.Value;
ThrowIfInvalidOptions(_jwtOptions);
}
Just a suggestion, the _jwtOptions.Expiration should be a timespan, i.e. how long the token should be valid for, so let's say it's 20 and in minutes, in that case you should have expires: DateTime.UtcNow.AddMinutes(_jwtOptions.Expiration) or something similar. And may be even rename it to reflect that.

Web Api to Signalr Azure token not working

I have a Asp.Net Web Api (Api1) that needs to send a message to a Signalr Realtime Api (Api2). I am trying to use Azure AD bearer tokens to authenticate. The client for Api1 is a JavaScript client that uses ADAL.js to get a token from Azure.
var authContext = new AuthenticationContext({
tenant: tenantId,
clientId: jsclientId,
postLogoutRedirectUri: window.location.origin,
cacheLocation: 'localStorage',
endpoints: {
api1Url: api1ResourceUri
}
});
authContext.acquireToken(jsclientId, function (error, token) {
if (error || !token) {
authContext.clearCache();
authContext.login();
}
});
The JS client attaches this token in the Authorization header to all Api calls to Api1. In Api1 I am using the following code to get an access token from Azure AD.
var userAssertion = new UserAssertion(bootstrapContext.Token, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
var result = await authenticationContext.AcquireTokenAsync(api2ResourceId, new ClientCredential(api1clientId, api1clientSecret), userAssertion);
I am attaching this access token to the request as an authorization header "Bearer tokenvalue". In the Signalr Hub Owin Startup class I have the following code.
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
TokenValidationParameters = new TokenValidationParameters
{
ValidAudiences = api1Audiences,
SaveSigninToken = true
},
Tenant = configSection.TenantId
});
While the ClaimsIdentity on the hub is showing as authenticated, the user's identity is not being set. identity.name is null. It looks like the users identity is not being passed on to the Signalr hub.
You're getting no User Identity in your API because you are authenticating to it as an application, not as a user.
The acquireTokenAsync overload that only takes in resource and ClientCredentials is for the Client Credentials flow (a.k.a App-only flow).
What you need to do is to use the On-behalf-of flow to swap the token you got for API1 for a token for API2.
So in API1's Startup.Auth, TokenValidation parameters, set Save SigninToken to true like so:
app.UseOpenIdConnectAuthentication(
OpenIdConnectAuthenticationOptions
{
// ...
TokenValidationParameters = new TokenValidationParameters
{
SaveSigninToken = true
},
// ...
});
And then wherever you want to call your API2, do as follows:
ClientCredential clientCred = new ClientCredential(clientId, appKey);
var bootstrapContext = ClaimsPrincipal.Current.Identities.First().BootstrapContext as System.IdentityModel.Tokens.BootstrapContext;
string userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null ? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value : ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;
string userAccessToken = bootstrapContext.Token;
UserAssertion userAssertion = new UserAssertion(bootstrapContext.Token, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
result = await authContext.AcquireTokenAsync(api2ResourceId, clientCred, userAssertion);
See it in the sample: https://github.com/Azure-Samples/active-directory-dotnet-webapi-onbehalfof, specificaly the ToDoService's Startup.Auth.cs and TodoListController.cs.
NOTE: This sample is for native app + web api, you're adapting it for web app + web api.
Edit - Make sure your JS code is requesting a token for your API1 by specifying API1 in the JS acquireToken call like so:
var authContext = new AuthenticationContext({
tenant: tenantId,
clientId: jsclientId,
postLogoutRedirectUri: window.location.origin,
cacheLocation: 'localStorage',
endpoints: {
api1Url: api1ResourceUri
}
});
authContext.acquireToken(api1clientId, function (error, token) {
if (error || !token) {
authContext.clearCache();
authContext.login();
}
});

Resources