In the Startup.cs of web api app, I have the following code. When this runs it throws NullReferenceException at GetAccessTokenForUserAsync. Any idea what I am missing?
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Events = new JwtBearerEvents()
{
OnTokenValidated = async ctx =>
{
var tokenAcquisition = ctx.HttpContext.RequestServices
.GetRequiredService<ITokenAcquisition>();
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(async (request) =>
{
var token = await tokenAcquisition
.GetAccessTokenForUserAsync(new[] { "User.Read", "User.ReadBasic.All", "GroupMember.Read.All" }, user: ctx.Principal);
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
}));
// Get user information from Graph
var groups = await graphClient.Me.CheckMemberGroups(new[] { Constants.GroupId })
.Request()
.PostAsync();
}
};
});
services
.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddMicrosoftIdentityWebApi(this.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph()
.AddInMemoryTokenCaches();
NullReferenceException:
Update:
I Just figured out that the issue is because I am missing the authenticationScheme: JwtBearerDefaults.AuthenticationScheme parameter value for the GetAccessTokenForUserAsync method call but now I am getting this exception message ""IDW10104: Both client secret and client certificate cannot be null or whitespace, and only ONE must be included in the configuration of the web app when calling a web API. For instance, in the appsettings.json file. "
Am I not using a token to call the endpoint? Why do I need a client secret or certificate? Can I do without? The token that the web API get from the caller is from an authenticated user and the web API has delegated permission of the user?
I am new to this so please bear with me. Thanks!
Update2:
After adding the client secret, I am getting the following exception:
Update3:
public void ConfigureServices(IServiceCollection services)
{
var principalProvider = new ClaimsPrincipalProvider();
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Events = new JwtBearerEvents()
{
OnTokenValidated = async ctx =>
{
try
{
var tokenAcquisition = ctx.HttpContext.RequestServices
.GetRequiredService<ITokenAcquisition>();
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(async (request) =>
{
var token = await tokenAcquisition
.GetAccessTokenForUserAsync(new[] { "User.Read", "User.ReadBasic.All", "GroupMember.Read.All" }, user: ctx.Principal, authenticationScheme: JwtBearerDefaults.AuthenticationScheme);
request.Headers.Authorization =
new AuthenticationHeaderValue("Bearer", token);
}));
var groups = await graphClient.Me.CheckMemberGroups(
new[]
{
Constants.GroupId
})
.Request()
.PostAsync();
}
catch (System.Exception ex)
{
}
}
};
});
services
.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddMicrosoftIdentityWebApi(this.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph()
.AddInMemoryTokenCaches();
services.AddAuthorization(options =>
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme);
defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
});
}
I tried to reproduce the scenario in my environment using postman.
I excluded client secret/certificate while getting the token to the application for the user.
I received similar error :
Basicallly, when Web apps call web APIs we need some valid secret token to give access to secure app.In general they are confidential client applications. So the secret that is registered in the azure ad for the app must be passed during the call to Azure AD endpoint to get a token.
Then I provided a valid value of secret to the endpoint and received to token successfully to call my Api.
In your app it must be included in
appsettings.json:
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "[xxxxx]",
"TenantId": "<tenantId here>"
// To call an API
"ClientSecret": "[Copy the client secret added to the app from the Azure portal]",
},
"MyApi": {
"BaseUrl": "https://graph.microsoft.com/beta",
"Scopes": "https://graph.microsoft.com/.default"
}
}
And then received the group members successfully.
Reference: Build a web app that authenticates users and calls web APIs - Microsoft Entra | Microsoft Learn
Related
When I try to login via Facebook, it throws the following error;
facebook has detected app isn't using a secure connection to transfer information
But I'm pretty sure that it is secured via 'Let's encrypt'.
I have checked Web and Client OAuth login boxes and set the corresponding redirect uris on developer facebook.
On maui side, I am calling the following code piece and AppSettings.BaseUrl is correct, I have checked that;
WebAuthenticatorResult authResult = await WebAuthenticator.Default.AuthenticateAsync(
new WebAuthenticatorOptions()
{
Url = new Uri($"{AppSettings.BaseUrl}account/authentication/{scheme}"),
CallbackUrl = new Uri("tibi://"),
PrefersEphemeralWebBrowserSession = true
});
And on backend side, the following api is requested;
[HttpGet("authentication/{scheme}")]
[AllowAnonymous]
public async Task Get([FromRoute] string scheme)
{
var auth = await Request.HttpContext.AuthenticateAsync(scheme);
if (!auth.Succeeded
|| auth?.Principal == null
|| !auth.Principal.Identities.Any(id => id.IsAuthenticated)
|| string.IsNullOrEmpty(auth.Properties.GetTokenValue("access_token")))
{
// Not authenticated, challenge
await Request.HttpContext.ChallengeAsync(scheme);
}
else
{
var claims = auth.Principal.Identities.FirstOrDefault()?.Claims;
var email = string.Empty;
email = claims?.FirstOrDefault(c => c.Type == System.Security.Claims.ClaimTypes.Email)?.Value;
// Get parameters to send back to the callback
var qs = new Dictionary<string, string>
{
{ "access_token", auth.Properties.GetTokenValue("access_token") },
{ "refresh_token", auth.Properties.GetTokenValue("refresh_token") ?? string.Empty },
{ "expires_in", (auth.Properties.ExpiresUtc?.ToUnixTimeSeconds() ?? -1).ToString() },
{ "email", email }
};
// Build the result url
var url = callbackScheme + "://#" + string.Join(
"&",
qs.Where(kvp => !string.IsNullOrEmpty(kvp.Value) && kvp.Value != "-1")
.Select(kvp => $"{WebUtility.UrlEncode(kvp.Key)}={WebUtility.UrlEncode(kvp.Value)}"));
// Redirect to final url
Request.Host = HostString.FromUriComponent(AppSettingsProvider.GatewayUrl);
Request.HttpContext.Response.Redirect(url);
}
}
Challenge is invoked successfully and redirects me to the Facebook login but when I sign in, I get the error above.
so I have a .net core (2.1) API that uses JWT tokens for authentication. I can login and make authenticated calls successfully.
I am using React (16.6.3) for the client, which getting a JWT code and making authenticated calls to the API works.
I am trying to add signalr hubs to the site. If I do not put an [Authorize] attribute on the hub class. I can connect, send and receive messages (its a basic chathub at the moment).
when I add the [Authorize] attribute to the class, the React app will make an HttpPost to example.com/hubs/chat/negotiate . I would get a 401 status code. the Authorization: Bearer abc..... header would be passed up.
To build the hub in React I use:
const hubConn = new signalR.HubConnectionBuilder()
.withUrl(`${baseUrl}/hubs/chat`, { accessTokenFactory: () => jwt })
.configureLogging(signalR.LogLevel.Information)
.build();
where the jwt variable is the token.
I have some setup for the authentication:
services.AddAuthentication(a =>
{
a.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
a.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = false;
options.Audience = jwtAudience;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtIssuer,
ValidAudience = jwtAudience,
RequireExpirationTime = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtKey)),
};
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var authToken = context.Request.Headers["Authorization"].ToString();
var token = !string.IsNullOrEmpty(accessToken) ? accessToken.ToString() : !string.IsNullOrEmpty(authToken) ? authToken.Substring(7) : String.Empty;
var path = context.HttpContext.Request.Path;
// If the request is for our hub...
if (!string.IsNullOrEmpty(token) && path.StartsWithSegments("/hubs"))
{
// Read the token out of the query string
context.Token = token;
}
return Task.CompletedTask;
}
};
});
the OnMessageReceived event does get hit and context.Token does get set to the JWT Token.
I can't figure out what I am doing wrong to be able to make authenticated calls for signalr core.
solution
I updated my code to use 2.2 (not sure if this was actually required).
so I spent some time looking at the source code, and the examples within:
https://github.com/aspnet/AspNetCore
I had a Signalr CORS issue which was solved with:
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.SetIsOriginAllowed((host) => true) //allow all connections (including Signalr)
);
});
the important part being .SetIsOriginAllowed((host) => true) This allows all connections for both website and signalr cors access.
I had not added
services.AddAuthorization(options =>
{
options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
{
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireClaim(ClaimTypes.NameIdentifier);
});
});
I had only used services.AddAuthentication(a =>
I took the following directly from the samples in github
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
if (!string.IsNullOrEmpty(accessToken) &&
(context.HttpContext.WebSockets.IsWebSocketRequest || context.Request.Headers["Accept"] == "text/event-stream"))
{
context.Token = context.Request.Query["access_token"];
}
return Task.CompletedTask;
}
};
Not sure if this was needed in the attribute, but the same used it on its hubs
[Authorize(JwtBearerDefaults.AuthenticationScheme)]
with that I could not get multiple website and console apps to connect and communicate via signalr.
To be used with [Authorize] you need to set the request header. Since web sockets do not support headers, the token is passed with the query string, which you correctly parse. The one thing that's missing is
context.Request.Headers.Add("Authorization", "Bearer " + token);
Or in your case probably context.HttpContext.Request.Headers.Add("Authorization", "Bearer " + token);
Example:
This is how i do it. On the client:
const signalR = new HubConnectionBuilder().withUrl(`${this.hubUrl}?token=${token}`).build();
On the server, in Startup.Configure:
app.Use(async (context, next) => await AuthQueryStringToHeader(context, next));
// ...
app.UseSignalR(r => r.MapHub<SignalRHub>("/hubUrl"));
Implementation of AuthQueryStringToHeader:
private async Task AuthQueryStringToHeader(HttpContext context, Func<Task> next)
{
var qs = context.Request.QueryString;
if (string.IsNullOrWhiteSpace(context.Request.Headers["Authorization"]) && qs.HasValue)
{
var token = (from pair in qs.Value.TrimStart('?').Split('&')
where pair.StartsWith("token=")
select pair.Substring(6)).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(token))
{
context.Request.Headers.Add("Authorization", "Bearer " + token);
}
}
await next?.Invoke();
}
best method for using authorization for hubs is to force the application add the jwt token from the query string to the context
and its working for me via this method
put this inside your program.cs (dot net 6):
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = false,
ValidateIssuerSigningKey = true
};
o.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
if (string.IsNullOrEmpty(accessToken) == false)
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
and my hubs are like this its using the main authorize method of asp
[Microsoft.AspNetCore.Authorization.Authorize]
public async Task myhub()
{
//do anything in your hub
}
youtube totorial that helped me to solve this problem
I'm trying to set up our IdentityServer solution to accept a custom Grant Validator. Our API project is accessed by to UIs, one that uses Password authentication (which is working) and now one that will use a 3rd party authentication.
In our API I've set up IdentityServer like so:
Startup.cs
public void Configuration(IAppBuilder app)
{
var factory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get());
var userService = new IdentityUserService();
factory.UserService = new Registration<IUserService>(resolver => userService);
factory.CustomGrantValidators.Add(
new Registration<ICustomGrantValidator, MyGrantValidator>());
var options = new IdentityServerOptions
{
SiteName = "My App Name",
SigningCertificate = Certificate.Get(),
Factory = factory
};
app.Map("/identity", identityServerApp =>
{
identityServerApp.UseIdentityServer(options);
});
}
MyGrantValidator.cs:
public class MyGrantValidator : ICustomGrantValidator
{
public async Task<CustomGrantValidationResult> ValidateAsync(ValidatedTokenRequest request)
{
// For now I just want a basic response. More logic will come later.
var authResult = new AuthenticateResult(
subject: "1234", // user.AccountId.ToString(),
name: "bob" //context.UserName
);
var grantResult = new CustomGrantValidationResult
{
IsError = authResult.IsError,
Error = authResult.ErrorMessage,
ErrorDescription = authResult.ErrorMessage,
Principal = authResult.User
};
return await Task.FromResult(grantResult);
}
public string GrantType => "myGrantType";
}
In my UI, I setup a client like this:
var owinContext = HttpContext.GetOwinContext();
var token = owinContext.Authentication.User.FindFirst(c => c.Type == "myToken")?.Value;
var tokenId = owinContext.Authentication.User.FindFirst(c => c.Type == ClaimTypes.Sid)?.Value;
var client = new TokenClient(
ConfigurationManager.AppSettings["IdentityServerBaseUrl"] + "/connect/token",
"MyUser",
ConfigurationManager.AppSettings["MyClientSecret"],
AuthenticationStyle.Custom
);
var tokenResponse = client.RequestCustomGrantAsync(
"myGrantType",
"read write",
new Dictionary<string, string>
{
{ "token", token },
{ "tokenId", tokenId }
}
).Result;
return Redirect(returnUrl);
When the Request is triggered, I get: unsupported_grant_type
What am I missing?
You're using a client called "MyUser" (weird name for a client, but ok). Is that client registered as one of the in-memory clients with grant type set to "custom"?
I created a custom authorizer for API Gateway so that i can pass the Facebook token and it will authenticate it using Cognito's Federated identity.
My problem is that the fb token seems to expire so I keep getting 403 errors. I am wondering if my approach is correct. Should I pass the Facebook token as part of the request header to API gateway on every REST API call or so I pass AWS identity id instead. Any feedback is appreciated. Thank you.
var AWS = require('aws-sdk');
var cognitoidentity = new AWS.CognitoIdentity();
exports.handler = (event, context, callback) => {
var params = {
IdentityPoolId: 'us-west-2:xxxxxxxxxxxxxxxxx’, /* required */
AccountId: ‘xxxxxxxxxxxxxxxxx,
Logins: {
'graph.facebook.com': event.authorizationToken //Token given by Facebook
}
};
console.log(event.methodArn);
cognitoidentity.getId(params, function(err, data) {
if (err) {
console.log(err);
callback(null, generatePolicy('user', 'Deny', event.methodArn));
}
else{
console.log("success");
callback(null, generatePolicy('user', 'Allow', event.methodArn));
}
});
};
var generatePolicy = function(principalId, effect, resource) {
var authResponse = {};
authResponse.principalId = principalId;
if (effect && resource) {
var policyDocument = {};
policyDocument.Version = '2012-10-17'; // default version
policyDocument.Statement = [];
var statementOne = {};
statementOne.Action = 'execute-api:Invoke'; // default action
statementOne.Effect = effect;
statementOne.Resource = resource;
policyDocument.Statement[0] = statementOne;
authResponse.policyDocument = policyDocument;
}
return authResponse;
}
I have following openid options:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationType = "oidc",
SignInAsAuthenticationType = "Cookies",
Authority = "http://localhost:5000",
ClientId = "mvcClient",
ClientSecret = "secret",
RedirectUri = "http://localhost:5002/signin-oidc",
PostLogoutRedirectUri = "http://localhost:5002",
ResponseType = "code id_token",
Scope = "openid profile",
Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var claims_to_exclude = new[]
{
"aud", "iss", "nbf", "exp", "nonce", "iat", "at_hash"
};
var claims_to_keep =
n.AuthenticationTicket.Identity.Claims
.Where(x => false == claims_to_exclude.Contains(x.Type)).ToList();
claims_to_keep.Add(new Claim("id_token", n.ProtocolMessage.IdToken));
if (n.ProtocolMessage.AccessToken != null)
{
claims_to_keep.Add(new Claim("access_token", n.ProtocolMessage.AccessToken));
}
}
}
}
I see n.ProtocolMessage.AccessToken is always null.
I configured client in identity server like this:
new Client()
{
ClientId = "mvcClient",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
ClientSecrets = new List<Secret>()
{
new Secret("secret".Sha256())
},
// RequireConsent = false,
// where to redirect to after login
RedirectUris = { "http://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:5002" },
AllowedScopes =
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
StandardScopes.OfflineAccess.Name,
StandardScopes.Roles.Name,
"API"
}
},
I want to know why n.ProtocolMessage.AccessToken is null and how can i get its value
UPDATE
If I change Client Type to Hybrid like this:
AllowedGrantTypes = GrantTypes.Hybrid,
and ResponseType = "code id_token token:
I get invalid_request error on server
If I try to get access token like this (in notifications):
var client = new TokenClient("http://localhost:5000/connect/token", "mvcClient", "secret");
var response = client.RequestClientCredentialsAsync("testscope").Result;
var accesstoken = response.AccessToken;
claims_to_keep.Add(new Claim("access_token", accesstoken));
The result token has only one scope(i.e testscope) instead of all other scopes defined for that client.
It's null because you're not asking for an access token.
ResponseType = "code id_token" means give the client a "Authoriziation Code" and a "Id token" on the callback. To receive an access token,either
include token in ResponseType as ResponseType = "code id_token token" & update the client flow to Hybrid flow (code + token), since that's what we're now doing.
or
fetch an access token using the /token endpoint using the "Authorization Code" available on the ProtocolMessage.
The access token should not be brought back along with code and id_token.
The right way to get it is through the back channel using client id and client secret.
Add this to the Notifications block:
AuthorizationCodeReceived = async n =>
{
var tokenClient = new TokenClient(n.Options.Authority + "/connect/token", "Client_Id", "secret");
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
else
{
string accessToken = tokenResponse.AccessToken;
//Other logic
}
}