ASP.Net Identity 2 - custom response from OAuthAuthorizationServerProvider - c#

This question is continuation of my previous one: ASP.Net Identity 2 login using password from SMS - not using two-factor authentication
I've build my custom OAuthAuthorizationServerProvider to support custom grant_type.
My idea was to create grant_type of sms that will allow user to generate one-time access code that will be send to his mobile phone and then user as password when sending request with grant_type of password.
Now after generating, storing and sending via SMS that password I'd like to return custom response, not token from my GrantCustomExtension.
public override async Task GrantCustomExtension(OAuthGrantCustomExtensionContext context)
{
const string allowedOrigin = "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] {allowedOrigin});
if (context.GrantType != "sms")
{
context.SetError("invalid_grant", "unsupported grant_type");
return;
}
var userName = context.Parameters.Get("username");
if (userName == null)
{
context.SetError("invalid_grant", "username is required");
return;
}
var userManager = context.OwinContext.GetUserManager<ApplicationUserManager>();
ApplicationUser user = await userManager.FindByNameAsync(userName);
if (user == null)
{
context.SetError("invalid_grant", "user not found");
return;
}
var generator = new TotpSecurityStampBasedTokenProvider<ApplicationUser, string>();
await userManager.UpdateSecurityStampAsync(user.Id);
var accessCode = await generator.GenerateAsync("SMS", userManager, user);
var accessCodeExpirationTime = TimeSpan.FromMinutes(5);
var result = await userManager.AddAccessCode(user, accessCode, accessCodeExpirationTime);
if(result.Succeeded)
{
Debug.WriteLine("Login code:"+accessCode);
//here I'll send login code to user phone via SMS
}
//return 200 (OK)
//with content type="application/json; charset=utf-8"
//and custom json content {"message":"code send","expires_in":300}
//skip part below
ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager, "SMS");
var ticket = new AuthenticationTicket(oAuthIdentity, null);
context.Validated(ticket);
}
How can I stop generating token and return custom response from OAuthAuthorizationServerProvider?
I'm aware of two methods: TokenEndpoint, TokenEndpointResponse, but I'd like to override whole response, not just token.
EDIT:
For now I'm creating temporary ClaimsIdentity in GrantCustomExtension using code below:
var ci = new ClaimsIdentity();
ci.AddClaim(new Claim("message","send"));
ci.AddClaim(new Claim("expires_in", accessCodeExpirationTime.TotalSeconds.ToString(CultureInfo.InvariantCulture)));
context.Validated(ci);
and I'm overriding TokenEndpointResponse:
public override Task TokenEndpointResponse(OAuthTokenEndpointResponseContext context)
{
if (context.TokenEndpointRequest.GrantType != "sms") return base.TokenEndpointResponse(context);
//clear response containing temporary token.
HttpContext.Current.Response.SuppressContent = true;
return Task.FromResult<object>(null);
}
This has two issues: when calling context.Validated(ci); I'm saying this is a valid user, but instead I'd like to response information that I've send access code via SMS.
HttpContext.Current.Response.SuppressContent = true; clears response, but I'd like to return something instead of empty response.

This is more of a workaround then a final solution, but I believe it is the most reliable way of solving your issue without rewriting tons of code from the default OAuthAuthorizationServerProvider implementation.
The approach is simple: use a Owin middleware to catch token requests, and overwrite the response if an SMS was sent.
[Edit after comments] Fixed the code to allow the response body to be buffered and changed as per this answer https://stackoverflow.com/a/36414238/965722
Inside your Startup.cs file:
public void Configuration(IAppBuilder app)
{
var tokenPath = new PathString("/Token"); //the same path defined in OAuthOptions.TokenEndpointPath
app.Use(async (c, n) =>
{
//check if the request was for the token endpoint
if (c.Request.Path == tokenPath)
{
var buffer = new MemoryStream();
var body = c.Response.Body;
c.Response.Body = buffer; // we'll buffer the response, so we may change it if needed
await n.Invoke(); //invoke next middleware (auth)
//check if we sent a SMS
if (c.Get<bool>("sms_grant:sent"))
{
var json = JsonConvert.SerializeObject(
new
{
message = "code send",
expires_in = 300
});
var bytes = Encoding.UTF8.GetBytes(json);
buffer.SetLength(0); //change the buffer
buffer.Write(bytes, 0, bytes.Length);
//override the response headers
c.Response.StatusCode = 200;
c.Response.ContentType = "application/json";
c.Response.ContentLength = bytes.Length;
}
buffer.Position = 0; //reset position
await buffer.CopyToAsync(body); //copy to real response stream
c.Response.Body = body; //set again real stream to response body
}
else
{
await n.Invoke(); //normal behavior
}
});
//other owin middlewares in the pipeline
//ConfigureAuth(app);
//app.UseWebApi( .. );
}
And inside your custom grant method:
// ...
var result = await userManager.AddAccessCode(user, accessCode, accessCodeExpirationTime);
if(result.Succeeded)
{
Debug.WriteLine("Login code:"+accessCode);
//here I'll send login code to user phone via SMS
}
context.OwinContext.Set("sms_grant:sent", true);
//you may validate the user or set an error, doesn't matter anymore
//it will be overwritten
//...

I would recommend to have a look at this answer :
https://stackoverflow.com/a/24090287/2508268
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
}
return Task.FromResult<object>(null);
}

Related

How to specific "Always redirect to external provider when login" in IdentityServer4?

When I want to login via Facebook for example for the first time, everything works. But, after a logout and an attempt to re-login in the same provider, redirection does not occur, and the system logs in using the previous account. How to specify "Always redirect"?
[HttpGet]
public async Task<IActionResult> Callback()
{
// read external identity from the temporary cookie
var result =
await HttpContext.AuthenticateAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception("External authentication error");
}
// lookup our user and external provider info
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result);
// retrieve return URL
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
if (user == null)
{
user = await AutoProvisionUser(provider, providerUserId, claims);
}
if (user.Email == null)
{
return RedirectToAction("EnterEmail", new { provider, providerUserId, returnUrl });
}
var additionalLocalClaims = new List<Claim>();
var localSignInProps = new AuthenticationProperties();
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
// issue authentication cookie for user
var principal = await _signInManager.CreateUserPrincipalAsync(user);
additionalLocalClaims.AddRange(principal.Claims);
var name = principal.FindFirst(JwtClaimTypes.Name)?.Value ?? user.Id;
var isuser = new IdentityServerUser(user.Id)
{
DisplayName = name,
IdentityProvider = provider,
AdditionalClaims = additionalLocalClaims
};
await HttpContext.SignInAsync(isuser, localSignInProps);
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.Id, name, true, context?.Client.ClientId));
if (context != null)
{
if (context.IsNativeClient())
{
// The client is native, so this change in how to
// return the response is for better UX for the end user.
return this.LoadingPage("Redirect", returnUrl);
}
}
return Redirect(returnUrl);
}
Logout:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout(LogoutInputModel model)
{
// build a model so the logged out page knows what to display
var vm = await BuildLoggedOutViewModelAsync(model.LogoutId);
vm.TriggerExternalSignout = true;
if (User?.Identity.IsAuthenticated == true)
{
// delete local authentication cookie
await _signInManager.SignOutAsync();
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
}
else
{
vm.TriggerExternalSignout = false;
}
// check if we need to trigger sign-out at an upstream identity provider
if (vm.TriggerExternalSignout)
{
// build a return URL so the upstream provider will redirect back
// to us after the user has logged out. this allows us to then
// complete our single sign-out processing.
string url = Url.Action("Logout", new { logoutId = vm.LogoutId });
// this triggers a redirect to the external provider for sign-out
return SignOut(new AuthenticationProperties { RedirectUri = url }, vm.ExternalAuthenticationScheme);
}
return View("LoggedOut", vm);
}
I tried clear cookie, but nothing changed. Then, i tried specify "prompt":"login" in AuthentificationProperties - noting changed.

Calling Microsoft Graph API to create an event, What am I doing wrong?

I've been trying to call Microsoft Graph API for creating events, however I've not been able to do it.
Context: I have a Web MVC application (C#) already in production, with the "common" authentication method, reading a database of users. Recently the customer asked me the possibility to create Microsoft Teams Meetings from the application and also those created meetings have to be scheduled in the Microsoft Teams Calendar with the "Join" button to enter the meeting.
I already configured the API permissions, client secret and used the other properties like tenant, user id, etc from the Azure Portal, I'm sharing a screenshot of my configuration. I'm doing the "Get access on behalf of a user" process.
API Permissions:
Permissions image
Taking the example of the authorize endpoint from the docs, of course I'm replacing the values with my own info
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize?
client_id=11111111-1111-1111-1111-111111111111
&response_type=code
&redirect_uri=http%3A%2F%2Flocalhost%2Fmyapp%2F
&response_mode=query
&scope=offline_access%20user.read%20mail.read
&state=12345
Here is my code to Receive the code once the user authorizes the permissions, I'm just storing the value in a static class for testing
public ActionResult ReceiveCode(string code)
{
AuthenticationConfig.Code = code;
//this.Code = code;
return RedirectToAction("Index");
}
Once I got the Auth code, I'm using it to create the event with the generated token, also I already verified that the token contains the permissions given in the Azure Portal.
This is the input for the /events endpoint
var json = JsonConvert.SerializeObject(new
{
subject = "Let's go for lunch",
body = new
{
contentType = "HTML",
content = "Does noon work for you?"
},
start = new
{
dateTime = "2017-04-15T12:00:00",
timeZone = "Pacific Standard Time",
},
end = new
{
dateTime = "2017-04-15T14:00:00",
timeZone = "Pacific Standard Time"
},
location = new
{
displayName = "Harry's Bar",
},
attendees = new List<Attendee>()
{
new Attendee
{
EmailAddress = new EmailAddress
{
Address = "mymail#whatever.com",
Name = "Foo Bar"
},
Type = AttendeeType.Required
}
},
allowNewTimeProposals = true,
isOnlineMeeting = true,
onlineMeetingProvider = "teamsForBusiness",
});
This is the complete method, for the json value, please see the json above. I also tried with the "me" url but it does not work either.
public async Task<ActionResult> OnlineMeeting()
{
try
{
var httpClient = new HttpClient();
var paramsDictionary = new Dictionary<string, string>();
paramsDictionary.Add("client_id",AuthenticationConfig.ClientId);
paramsDictionary.Add("scope", "Calendars.ReadWrite");
paramsDictionary.Add("code", AuthenticationConfig.Code);
paramsDictionary.Add("redirect_uri", "https://localhost:44379/Meeting/Reunion/ReceiveCode");
paramsDictionary.Add("grant_type", "authorization_code");
paramsDictionary.Add("client_secret", AuthenticationConfig.ClientSecret);
var url = string.Format("https://login.microsoftonline.com/{0}/oauth2/v2.0/token", "tenant");
var response = await httpClient.PostAsync(url, new FormUrlEncodedContent(paramsDictionary));
if (response.IsSuccessStatusCode)
{
var jsonResponse = await response.Content.ReadAsStringAsync();
var jsonResult = JsonConvert.DeserializeObject(jsonResponse) as JObject;
var accessToken = jsonResult.GetValue("access_token").ToString();
httpClient = new HttpClient();
var json = JsonConvert.SerializeObject(new { });
var defaultRequestHeaders = httpClient.DefaultRequestHeaders;
if (defaultRequestHeaders.Accept == null || !defaultRequestHeaders.Accept.Any(m => m.MediaType == "application/json"))
{
httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var data = new StringContent(json);
response = await httpClient.PostAsync("https://graph.microsoft.com/v1.0/users/{user id}/events", data);
if (response.IsSuccessStatusCode)
{
// Nice
}
else
{
Console.WriteLine($"Failed to call the web API: {response.StatusCode}");
string content = await response.Content.ReadAsStringAsync();
}
}
else
{
Console.WriteLine($"Failed to call the web API: {response.StatusCode}");
string content = await response.Content.ReadAsStringAsync();
}
}
catch (Exception ex)
{
}
return View();
}
I'm able to the get the token, but when trying to create the event returns the next response.
{
"error": {
"code": "ResourceNotFound",
"message": "Resource could not be discovered.",
"innerError": {
"date": "2021-08-31T22:58:18",
"request-id": "c5c07afa-fa89-4948-a9f8-f80ca4cbafc3",
"client-request-id": "c5c07afa-fa89-4948-a9f8-f80ca4cbafc3"
}
}
}
Am I missing something? Maybe the wrong endpoint?
Please, help.
Thanks in advance.

ADB2C - Handling the "Null user was passed in AcquiretokenSilent API" error when token cache expires

I've managed to configure my application to authenticate using ADB2C, and it seems to work fine. The ADB2C code implemented is a tweak of one of Microsoft's samples, in which they use a SessionTokenCache class to manage instances of TokenCache. In my application, I retrieve the access token as follows:
private async Task<string> _getAccessToken(IConfidentialClientCredentials credentials)
{
if (this.HasCredentials())
{
var clientCredential = new ClientCredential(credentials.ClientSecret);
var userId = this._getUserIdClaimValue();
var tokenCache = new SessionTokenCache(_httpContextResolver.Context, userId);
var confidentialClientApplication = new ConfidentialClientApplication(
credentials.ClientId,
credentials.Authority,
credentials.RedirectUri,
clientCredential,
tokenCache.GetInstance(),
null);
IAccount account = confidentialClientApplication.GetAccountsAsync().Result.FirstOrDefault();
if (account == null)
{
return "";
}
var authenticationResult = await confidentialClientApplication.AcquireTokenSilentAsync(
credentials.ApiScopes.Split(' '),
account,
credentials.Authority,
false);
return authenticationResult.AccessToken;
}
else
{
return "";
}
}
This method is used to get the access token and pass it in the request header of an HttpClient as follows:
...
using (var request = new HttpRequestMessage(HttpMethod.Get, address.AbsoluteUri))
{
if (this.HasCredentials())
{
string accessToken = await this._getAccessToken(_confidentialClientCredentials);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
}
using (HttpResponseMessage response = await this.SendAsync(request))
{
//result-processing logic
}
...
The problem is that when the app is restarted, the user remains authenticated through the ADB2C cookie, but confidentialClientApplication.GetAccountsAsync().Result.FirstOrDefault(); returns null. This probably happens because the token cache is destroyed on app restart, so I can probably use a Redis cache to fix.
My main issue however is how to handle the situation of having a null Account but being "authenticated" at the same time. How are my requests to my website being authenticated even though I have a null Account? Shouldn't it fail and redirect me to login page, for example?
I tried looking into Authorization filters, and using the following code to hook up to the auth process and validate if user is null there, but to no avail. The following events are not being called ever (this is in ConfigureServices):
services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
.AddAzureADB2C(options => Configuration.Bind("ActiveDirectoryB2C", options))
.AddAzureADB2CBearer(options => Configuration.Bind("ActiveDirectoryB2C", options))
.AddCookie((options) => new CookieAuthenticationOptions
{
Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
// context.Principal gives you access to the logged-in user
// context.Properties.GetTokens() gives you access to all the tokens
return Task.CompletedTask;
},
OnSignedIn = context =>
{
return Task.CompletedTask;
}
}
});
It all feels a bit too abstracted for me to make any sense of what's going on. Either that, or I'm missing something fundamental.
Note: The error "Null user was passed in AcquiretokenSilent API. Pass in a user object or call acquireToken authenticate.
" is thrown if I try to pass the null account to the confidentialClientApplication.AcquireTokenSilentAsync() method.
I solved with this code:
protected override void OnException(ExceptionContext filterContext)
{
if (filterContext.Exception is Microsoft.Identity.Client.MsalUiRequiredException)
{
RedirectToAction("SignIn", "Account");
}
else {
//Do your logging
// ...
}
}
I'll search for a better solution.

The token response was successfully returned: unsupported_grant_type

I'm migrating from .NET Core 1.1 to 2.0, and now I have to update my Authentication too.
I'm using OAuth and OpenIddict to .NET Core 2.0
When I'm sending the request to my connect/token I'm getting this:
OpenIddict.Server.OpenIddictServerHandler[0] The token response was
successfully returned: {
"error": "unsupported_grant_type",
"error_description": "The specified 'grant_type' parameter is not
supported."
}.
This is my request method:
using (var client = new HttpClient())
{
var request = new HttpRequestMessage(HttpMethod.Post, $"{url}/connect/token");
request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = clientId,
["client_secret"] = clientSecret,
["pessoaid"] = pessoaId,
["usuarioid"] = usuarioId,
["conta"] = conta,
["cpfcnpj"] = userDoubleCpf,
["fonteDados"] = fonteDados,
["userIdsLogged"] = userIdsLogged
});
var response = await client.SendAsync(request, HttpCompletionOption.ResponseContentRead);
response.EnsureSuccessStatusCode();
var result = JObject.Parse(await response.Content.ReadAsStringAsync());
if (result["error"] != null)
{
throw new InvalidOperationException("An error occurred while retrieving an access token.");
}
return result;
}
My OpenIddictApplications is generated when an application is linked to the user account, so the ClientId and Secret is generated, when a login request is send to my API and retrieve the respective values.
I have folowed the oppeniddict documentation and I have included everything in my Startup.cs
This is my AuthorizationController:
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request)
{
Debug.Assert(request.IsTokenRequest(),
"The OpenIddict binder for ASP.NET Core MVC is not registered. " +
"Make sure services.AddOpenIddict().AddMvcBinders() is correctly called.");
if (request.IsClientCredentialsGrantType())
{
// Note: the client credentials are automatically validated by OpenIddict:
// if client_id or client_secret are invalid, this action won't be invoked.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId, HttpContext.RequestAborted);
if (application == null)
{
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.InvalidClient,
ErrorDescription = "The client application was not found in the database."
});
}
// Create a new authentication ticket.
var ticket = CreateTicket(request, application);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
return BadRequest(new OpenIdConnectResponse
{
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}
I'm generating the AuthenticationTicket and returning this.
Any idea about what might be causing this kind of badrequest when I try to send the request to take my token?
This happens because you do not configure the client credentials flow on you Startup.cs.
See the example: https://github.com/openiddict/openiddict-samples/blob/dev/samples/ClientCredentialsFlow/AuthorizationServer/Startup.cs
Attention for line 52:
// Enable the client credentials flow.
options.AllowClientCredentialsFlow();

Want to request authorization to gmail with dotnetOpenAuth

I use dotnetOpenAuth. I want to request authorization to the user's gamil.
Do I need to use openId first?
Cannot find a decent tutorail. Can anyone help?
Tried this code unsuccesfully. Anyway I don't seems to ask for Gmail scope at the auth request, so I'm confused
public void PrepareAuthorizationRequest(Uri authCallbakUrl)
{
var consumer = new WebConsumer(GoogleConsumerConsts.ServiceDescription, mConsumerTokenManager);
// request access
consumer.Channel.Send(consumer.PrepareRequestUserAuthorization(authCallbakUrl, null, null));
throw new NoRedirectToAuthPageException();
}
public ProcessAuthorizationRequestResponse ProcessAuthorizationRequest()
{
ProcessAuthorizationRequestResponse response;
// Process result from the service provider
var consumer = new WebConsumer(GoogleConsumerConsts.ServiceDescription, mConsumerTokenManager);
var accessTokenResponse = consumer.ProcessUserAuthorization();
// If we didn't have an access token response, this wasn't called by the service provider
if (accessTokenResponse == null)
response = new ProcessAuthorizationRequestResponse
{
IsAuthorized = false
};
else
{
// Extract the access token
string accessToken = accessTokenResponse.AccessToken;
response = new ProcessAuthorizationRequestResponse
{
IsAuthorized = true,
Token = accessToken,
Secret = mConsumerTokenManager.GetTokenSecret(accessToken)
};
}
return response;
}
private string Test2()
{
// Process result from linked in
var google = new WebConsumer(GoogleConsumerConsts.ServiceDescription, mConsumerTokenManager);
// var accessToken = GetAccessTokenForUser();
var accessToken = String.Empty;
// Retrieve the user's profile information
var endpoint = GoogleConsumerConsts.GetGmailFeedsEndpoint;// new MessageReceivingEndpoint("http://api.linkedin.com/v1/people/~", HttpDeliveryMethods.GetRequest);
var request = google.PrepareAuthorizedRequest(endpoint, accessToken);
var response = request.GetResponse();
return (new StreamReader(response.GetResponseStream())).ReadToEnd();
}
No, you don't need to use OpenID if you just want to access the user's Gmail. OpenID is for when you want to authenticate the user. OAuth is for when you want to access the user's data.
You need to include the scope parameter in your authorization request as described in this question: Adding scopes to OAuth 1.0 authorization request with DotNetOpenAuth.

Categories

Resources