Blazor Server App - Cookie can not be set - c#

I'm working on a project to demonstrate Authorization code flow. Therefore I don't want to use any library that handles the authentication for me, but I want to make the whole process myself.
I created an Blazor Server app (SignalR).
On index page there is a single "Connect" button, that starts the whole authentication process and it is as follows.
Index.razor
//on button click
protected async Task ConnectClick()
{
await ConnectService.Connect();
}
ConnectService.cs
public void CreateSession()
{
if(!_httpContextAccessor.HttpContext.Request.Cookies.TryGetValue("userId", out string userId))
{
_httpContextAccessor.HttpContext.Response.Cookies.Append("userId", Guid.NewGuid().ToString());
}
}
public async Task Connect()
{
CreateSession();
//Generate random string as "state" parameter for ACF
var state = Guid.NewGuid().ToString();
var authorizeArgs = new Dictionary<string, string>
{
{"client_id", ...},
{"scope", ...},
{"redirect_uri", ".../Auth/ConnectCallback"},
{"response_type", "code"},
{"state", state}
};
//Save state to cookie to verify in later step
_httpContextAccessor.HttpContext.Response.Cookies.Append("state", state);
var url = ... //prepare url, not important
_navigationManager.NavigateTo(url);
}
public async Task ConnectCallback(string code, string state)
{
//Verify state
if(!_httpContextAccessor.HttpContext.Request.Cookies.TryGetValue("state", out string stateValue) || stateValue != state)
{
throw new AuthenticationException();
}
... //rest of authentication steps
_httpContextAccessor.HttpContext.Request.Cookies.TryGetValue("userId", out string userId);
_memoryCache.Set(userId, access_token);
_navigationManager.NavigateTo("/mypage");
}
ConnectCallback.razor
#page "/Auth/ConnectCallback"
...
#code {
protected override async Task OnInitializedAsync()
{
await AuthService.ConnectCallback(HttpContextAccessor.HttpContext.Request.Query["code"][0], HttpContextAccessor.HttpContext.Request.Query["state"][0]);
}
I know that a library would handle this in much more cleaner way, but the goal is to show the flow in a small demo app.
This is the latest state. I don't know if it is better to save the access token directly in the browser, but for now I keep it in the memory paired with userId.
What happens in this case is whenever I try to append a cookie I will receive:
System.InvalidOperationException: Headers are read-only, response has already started.
Now, I understand I'm doing something wrong. Does anyone know what would be the proper way to this, or what am I doing wrong here? I don't seem to find any solution to this anywhere.

You can create a CookieController that you will use for cookie management and redirect to it.
IsEssential indicates that the cookie is necessary for the website to function correctly.
[Route("[controller]")]
[ApiController]
public class CookieController : ControllerBase
{
[HttpGet("SetStateCookie")]
public async Task<ActionResult> SetStateCookie()
{
CookieOptions opt = new CookieOptions
{
IsEssential = true
};
Response.Cookies.Append("state", $"{Guid.NewGuid()}", opt);
return Redirect("/");
}
}

Related

How to create a 'CallBack' action after getting a Code from Authentication Server in OAuth2 in Web Application Controller

I'm Providing an 'OAuthHandler' for Walmart and overriding some OAuthHandler methods to communicate between User Agent (Client) and Remote Authenticate Server.
Below is my controller:
[AllowAnonymous]
public class WalmartLoginController : Controller
{
public async Task<IActionResult> Login([FromForm] string provider)
{
if (string.IsNullOrWhiteSpace(provider))
{
return BadRequest();
}
if (!await HttpContext.IsProviderSupportedAsync(provider))
{
return BadRequest();
}
return Challenge(new AuthenticationProperties { RedirectUri = "/" }, provider);
}
[HttpGet("~/signout")]
[HttpPost("~/signout")]
public IActionResult SignOutCurrentUser()
{
return SignOut(new AuthenticationProperties { RedirectUri = "/" },
CookieAuthenticationDefaults.AuthenticationScheme);
}
}
And I added some classes to handle my requests: [here is the importance of those that overrode from the OAuthHandler class]
public partial class WalmartAuthenticationHandler : OAuthHandler<WalmartAuthenticationOptions>
{
public WalmartAuthenticationHandler(
[NotNull] IOptionsMonitor<WalmartAuthenticationOptions> options,
[NotNull] ILoggerFactory logger,
[NotNull] UrlEncoder encoder,
[NotNull] ISystemClock clock)
: base(options, logger, encoder, clock)
{}
// STEP 1: CREATE CHALLENGE URL
protected override string BuildChallengeUrl([NotNull] AuthenticationProperties properties, [NotNull] string redirectUri)
{
var scopeParameter = properties.GetParameter<ICollection<string>>(OAuthChallengeProperties.ScopeKey);
var scope = scopeParameter != null ? FormatScope(scopeParameter) : FormatScope();
var parameters = new Dictionary<string, string?>
{
["client_id"] = Options.ClientId,
["response_type"] = "code",
["scope"] = scope
};
if (Options.UsePkce)
{
var bytes = BitConverter.GetBytes(256 / 8);
var codeVerifier = WebEncoders.Base64UrlEncode(bytes);
// Store this for use during the code redemption.
properties.Items.Add(OAuthConstants.CodeVerifierKey, codeVerifier);
var challengeBytes = SHA256.HashData(Encoding.UTF8.GetBytes(codeVerifier));
var codeChallenge = WebEncoders.Base64UrlEncode(challengeBytes);
parameters[OAuthConstants.CodeChallengeKey] = codeChallenge;
parameters[OAuthConstants.CodeChallengeMethodKey] = OAuthConstants.CodeChallengeMethodS256;
}
var state = Options.StateDataFormat.Protect(properties);
parameters["redirect_uri"] = QueryHelpers.AddQueryString(redirectUri, "state", state);
return QueryHelpers.AddQueryString(Options.AuthorizationEndpoint, parameters);
}
// STEP 2 : CHANGE CODE WITH ACCESS_TOKEN
protected override async Task<OAuthTokenResponse> ExchangeCodeAsync([NotNull] OAuthCodeExchangeContext context)
{
var tokenRequestParameters = new Dictionary<string, string?>()
{
["client_id"] = Options.ClientId,
["client_secret"] = Options.ClientSecret,
["redirect_uri"] = context.RedirectUri,
["code"] = context.Code,
["grant_type"] = "authorization_code"
};
// Add CodeVerify to tokenRequestParameters
if (context.Properties.Items.TryGetValue(OAuthConstants.CodeVerifierKey, out var codeVerifier))
{
tokenRequestParameters.Add(OAuthConstants.CodeVerifierKey, codeVerifier);
context.Properties.Items.Remove(OAuthConstants.CodeVerifierKey);
}
var endpoint = QueryHelpers.AddQueryString(Options.TokenEndpoint, tokenRequestParameters);
using var request = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-www-form-urlencoded"));
request.Content = new FormUrlEncodedContent(tokenRequestParameters);
using var response = await Backchannel.SendAsync(request, Context.RequestAborted);
if (!response.IsSuccessStatusCode)
{
// An error occurred while retrieving an OAuth token.
}
var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted));
return OAuthTokenResponse.Success(payload);
}
// STEP 3: access to UserInformation with Access Token
protected override async Task<AuthenticationTicket> CreateTicketAsync(
[NotNull] ClaimsIdentity identity,
[NotNull] AuthenticationProperties properties,
[NotNull] OAuthTokenResponse tokens)
{
using var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
using var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
if (!response.IsSuccessStatusCode)
{
// An error occurred while retrieving the user profile.
}
using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted));
var principal = new ClaimsPrincipal(identity);
var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement.GetProperty("data"));
context.RunClaimActions();
await Events.CreatingTicket(context);
return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name);
}
}
As you know, after running https:\\Development-Domain.com\signin by post method (second action), a Challenge is started and will redirect to BuildChallengeUrl() and return a Url with Code and State. The next action would be ExchangeCodeAsync() to change the Code sent by the remote server with an AccessToken.
The question is, which action or method was missed in this process? Will my ExchangeCodeAsync() call automatically after the BuildChallengeUrl() handler, or do I need to put some action to continue to authorize the user?
Update #1: I added a callback action to get info from 'QueryString' in controller to call next method of handler class:
[HttpPost("~/signin-oidc")]
public async Task<IActionResult> Callback([FromForm] string provider)
{
var code = Request.Query["code"];
var state = Request.Query["state"];
if (string.IsNullOrWhiteSpace(code))
{
return BadRequest();
}
if (!await HttpContext.IsProviderSupportedAsync(provider))
{
return BadRequest();
}
return {?};
}
What code must I write in {?} to request for Access Token from TokenEndpoint?
Except callback action other code are in-line with Authorisation code flow.
In PKCE Authorization code flow, you should match with flow mentioned here, that will help pass security validation / testing post production.
How it works, Authorization Flow
Answer to question,
CallBack action an endpoint action method of callback url should trigger and make post request to token endpoint of authorisation server with code received as query string in CallBack Url with Code Verifier created with Code Challenge and on successful post it will return access-token, refresh-token, id-token, .... based on your configuration. You will need to specify callback url also with client_id and client_secret in configuration.
re-arranging code based on this How it works, Authorization Flow will help.
After figuring out and researching Microsoft .Net Core OAuth libraries, I got the answer and I would like to share it with you:
As you know, we no need to write any code in callback action for running the next method in the WalmartAuthenticationHandler class because all the process is automatic and event-based and all of them are provided in the Microsoft OAuthHandler class. You need to write some code inside the Callback action just for registering User after the authentication process.
Even you can use Microsoft ExternalLogin Identity pages for the registration of an external User.
Also, I decided to create a project on GitHub and I'd like to share it here
OktaProvider, maybe everyone needs to add external authentication for non-famous companies.

JWT doesn't get stored in ASP.NET Core with Blazor

I followed this tutorial: https://medium.com/#st.mas29/microsoft-blazor-web-api-with-jwt-authentication-part-1-f33a44abab9d
I downloaded the example: https://github.com/StuwiiDev/DotnetCoreJwtAuthentication/tree/Part2
I can see that the token is created but I don't understand how it is or should be saved on the client side as each time I access the SampleDataController, which has the Authorize tag, it returns a 401.
When calling and adding the token using Postman it works.
What am I missing for my user to be authenticated? Doesn't Microsoft.AspNetCore.Authentication.JwtBearer handle the client part (storing the token)?
What am I missing for my user to be authenticated? Doesn't Microsoft.AspNetCore.Authentication.JwtBearer handle the client part (storing the token)?
The JwtBearer runs on server side , it will only validate the authorization header of request, namely Authorization: Bearer your_access_token, and won't care about how you WebAssembly codes runs . So you need send the request with a jwt accessToken . Since the tutorial suggests you should use localStorage , let's store the accessToken with localStorage .
Because WebAssembly has no access to BOM yet, we need some javascript codes served as glue . To do that, add a helper.js under the JwtAuthentication.Client/wwwroot/js/ :
var wasmHelper = {};
wasmHelper.ACCESS_TOKEN_KEY ="__access_token__";
wasmHelper.saveAccessToken = function (tokenStr) {
localStorage.setItem(wasmHelper.ACCESS_TOKEN_KEY,tokenStr);
};
wasmHelper.getAccessToken = function () {
return localStorage.getItem(wasmHelper.ACCESS_TOKEN_KEY);
};
And reference the script in your JwtAuthentication.Client/wwwroot/index.html
<body>
<app>Loading...</app>
<script src="js/helper.js"></script>
<script src="_framework/blazor.webassembly.js"></script>
</body>
Now, let's wrap the javascript codes into C# . Create a new file Client/Services/TokenService.cs:
public class TokenService
{
public Task SaveAccessToken(string accessToken) {
return JSRuntime.Current.InvokeAsync<object>("wasmHelper.saveAccessToken",accessToken);
}
public Task<string> GetAccessToken() {
return JSRuntime.Current.InvokeAsync<string>("wasmHelper.getAccessToken");
}
}
Register this service by :
// file: Startup.cs
services.AddSingleton<TokenService>(myTokenService);
And now we can inject the TokenService into Login.cshtml and use it to save token :
#using JwtAuthentication.Client.Services
// ...
#page "/login"
// ...
#inject TokenService tokenService
// ...
#functions {
public string Email { get; set; } = "";
public string Password { get; set; } = "";
public string Token { get; set; } = "";
/// <summary>
/// response from server
/// </summary>
private class TokenResponse{
public string Token;
}
private async Task SubmitForm()
{
var vm = new TokenViewModel
{
Email = Email,
Password = Password
};
var response = await Http.PostJsonAsync<TokenResponse>("http://localhost:57778/api/Token", vm);
await tokenService.SaveAccessToken(response.Token);
}
}
Let's say you want to send data within FetchData.cshtml
#functions {
WeatherForecast[] forecasts;
protected override async Task OnInitAsync()
{
var token = await tokenService.GetAccessToken();
Http.DefaultRequestHeaders.Add("Authorization",String.Format("Bearer {0} ",token));
forecasts = await Http.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");
}
}
and the result will be :
Apologies in advance as this is somewhat responding to a previous answer, but I don't have the rep to comment on that.
If it helps anyone else who was similarly looking for a solution to using JWT in a Blazor app, I found #itminus answer incredibly useful, but it also pointed me to another course.
One problem I found was that calling FetchData.cshtml a second time would blow up when it tries to add the Authorization header a second time.
Instead of adding the default header there, I added it to the HttpClient singleton after a successful login (which I believe Blazor creates for you automatically). So changing SubmitForm in Login.cshtml from #itminus' answer.
protected async Task SubmitForm()
{
// Remove any existing Authorization headers
Http.DefaultRequestHeaders.Remove("Authorization");
TokenViewModel vm = new TokenViewModel()
{
Email = Email,
Password = Password
};
TokenResponse response = await Http.PostJsonAsync<TokenResponse>("api/Token/Login", vm);
// Now add the token to the Http singleton
Http.DefaultRequestHeaders.Add("Authorization", string.Format("Bearer {0} ", response.Token));
}
Then I realised, than as I'm building a SPA, so I didn't need to persist the token across requests at all - it's just in attached to the HttpClient.
The following class handle the login process on the client, storing the JWT token in local storage. Note: It is the developer responsibility to store the JWT token, and passes it to the server. The client (Blazor, Angular, etc.) does not do that for him automatically.
public class SignInManager
{
// Receive 'http' instance from DI
private readonly HttpClient http;
public SignInManager(HttpClient http)
{
this.http = http;
}
[Inject]
protected LocalStorage localStorage;
public bool IsAuthenticated()
{
var token = localStorage.GetItem<string>("token");
return (token != null);
}
public string getToken()
{
return localStorage.GetItem<string>("token");
}
public void Clear()
{
localStorage.Clear();
}
// model.Email, model.Password, model.RememberMe, lockoutOnFailure: false
public async Task<bool> PasswordSignInAsync(LoginViewModel model)
{
SearchInProgress = true;
NotifyStateChanged();
var result = await http.PostJsonAsync<Object>("/api/Account", model);
if (result)// result.Succeeded
{
_logger.LogInformation("User logged in.");
// Save the JWT token in the LocalStorage
// https://github.com/BlazorExtensions/Storage
await localStorage.SetItem<Object>("token", result);
// Returns true to indicate the user has been logged in and the JWT token
// is saved on the user browser
return true;
}
}
}
// This is how you call your Web API, sending it the JWT token for // the current user
public async Task<IList<Profile>> GetProfiles()
{
SearchInProgress = true;
NotifyStateChanged();
var token = signInManager.getToken();
if (token == null) {
throw new ArgumentNullException(nameof(AppState)); //"No token";
}
this.http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// .set('Content-Type', 'application/json')
// this.http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
Profiles = await this.http.GetJsonAsync<Profile[]>("/api/Profiles");
SearchInProgress = false;
NotifyStateChanged();
}
// You also have to set the Startup class on the client as follows:
public void ConfigureServices(IServiceCollection services)
{
// Add Blazor.Extensions.Storage
// Both SessionStorage and LocalStorage are registered
// https://github.com/BlazorExtensions/Storage
**services.AddStorage();**
...
}
// Generally speaking this is what you've got to do on the client. // On the server, you've got to have a method, say in the Account controller, whose function is to generate the JWT token, you've to configure the JWT middleware, to annotate your controllers with the necessary attribute, as for instance:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
and so on...
Hope this helps...

Twilio Request Validation always fail for Voice Call (but works for SMS)

I have implemented a AuthorizationHandler according to an official Twilio tutorial but it only works for SMS-related requests but not voice-related requests (always fail the validation).
Below is the one and only AuthorizationHandler applied to different controllers that accept POST request from Twilio to notify my API of inbound and outbound voice calls, inbound SMS, and status change to outbound SMS:
public class TwilioInboundRequestAuthorizationHandler : AuthorizationHandler<TwilioInboundRequestRequirement>
{
private readonly RequestValidator _requestValidator;
public TwilioInboundRequestAuthorizationHandler(IOptionsSnapshot<AppOptions> options)
{
// Initialize the validator
_requestValidator = new RequestValidator(options.Value.TwilioAuthToken);
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TwilioInboundRequestRequirement requirement)
{
if (context.Resource is AuthorizationFilterContext mvcContext)
{
// Examine MVC-specific things like routing data.
HttpRequest httpRequest = mvcContext.HttpContext.Request;
if (IsValidRequest(httpRequest))
{
context.Succeed(requirement);
}
else
{
/* Omitted some code that logs the error to a cloud service */
context.Fail();
}
}
else
{
throw new NotImplementedException();
}
// Check if the requirement is fulfilled.
return Task.CompletedTask;
}
private bool IsValidRequest(HttpRequest request) {
// The Twilio request URL
var requestUrl = RequestRawUrl(request);
var parameters = ToDictionary(request.Form);
// The X-Twilio-Signature header attached to the request
var signature = request.Headers["X-Twilio-Signature"];
return _requestValidator.Validate(requestUrl, parameters, signature);
}
private static string RequestRawUrl(HttpRequest request)
{
return $"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}";
}
private static IDictionary<string, string> ToDictionary(IFormCollection collection)
{
return collection.Keys
.Select(key => new { Key = key, Value = collection[key] })
.ToDictionary(p => p.Key, p => p.Value.ToString());
}
}
public class TwilioInboundRequestRequirement : IAuthorizationRequirement
{
}
EDIT:
According to a suggestion from Twilio Support, I should change the RequestRawUrl to strip away the port number from the URL. However, that causes the validation working for voice calls only, while for SMS it doesn't work anymore (opposite to the original issue). I suspect Twilio has been setting an incorrect signature in the request header for either voice or SMS.
I changed the RequestRawUrl function from
private static string RequestRawUrl(HttpRequest request)
{
return $"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}";
}
to
private static string RequestRawUrl(HttpRequest request)
{
return $"{request.Scheme}://{request.Host.Host}{request.Path}{request.QueryString}";
}
We had a similar problem. What it boiled down to was that for some methods, (like SMS) the request URL comes in as https, and other methods (like TwiML voice script callbacks) the request comes in written as http.
If the request comes in written as http, the .Validate(...) method will fail on you, even though it is a valid request.
So to get the Twilio request validator to work, we simply rewrite the request URL.
private bool IsValidRequest(HttpRequestBase request)
{
var signature = request.Headers["X-Twilio-Signature"];
Debug.WriteLine(request.Headers["X-Twilio-Signature"]);
var requestUrl = rewriteUri(request.Url.AbsoluteUri);
Debug.WriteLine("URI is: " + rewriteUri(request.Url.AbsoluteUri));
return _requestValidator.Validate(requestUrl, request.Form, signature);
}
private string rewriteUri(string absoluteUri)
{
//check to make sure we're not replacing 'https' with 'httpss'
if (!absoluteUri.Contains("https"))
{
return Regex.Replace(absoluteUri, #"http", "https");
}
return absoluteUri;
}
I was having the exact same issue when running locally behind ngrok, but not when deployed. I figured out that the RequestValidator expects the original ngrok URL, but by default we are passing localhost. I ended up solving it like so:
private bool IsValidRequest(HttpRequest request)
{
var requestUrl = RequestRawUrl(request);
var parameters = ToDictionary(request.Form);
var signature = request.Headers["X-Twilio-Signature"];
// Check if we are running locally and need to pass ngrok through for validation to succeed.
if (request.Headers.ContainsKey("X-Original-Host") && request.Headers["X-Original-Host"][0].Contains("ngrok"))
{
requestUrl = requestUrl.Replace(request.Headers["Host"][0], request.Headers["X-Original-Host"][0]);
}
return _requestValidator.Validate(requestUrl, parameters, signature);
}
If you using ngrok, verifier expects ngrok URL (i think same thing with other proxies)
In PHP the original host can be get from HTTP_X_ORIGINAL_HOST, maybe this would help you somehow

How to implement the OAuth2 Auth_Code flow?

Purpose:
I work with a third party that has implemented an OAuth2 authorization code flow. The goal is for an authenticated user in my app to be able to access areas of a this third parties app without having to sign in to the third party. I have provided the third party with our authorization url, a clientId and a callback url (not sure how this is used).
I am trying to wire up a basic authorization code flow using OAuth2, but I am stuck on an invalid_grant error and am having trouble determining how the process is intended to work.
Here is what I have implemented thus far:
In the startup class I configure my OAuth server to include an AuthorizeEndpointPath and an AuthorizationCodeProvider:
var allowInsecureHttp = bool.Parse(ConfigurationManager.AppSettings["AllowInsecureHttp"]);
var oAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = allowInsecureHttp,
TokenEndpointPath = new PathString("/oauth2/token"),
AuthorizeEndpointPath = new PathString("/oauth2/authorize"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30),
Provider = new CustomOAuthProvider(HlGlobals.Kernel),
AccessTokenFormat = new CustomJwtFormat(_baseUrl, HlGlobals.Kernel),
AuthorizationCodeProvider = new SimpleAuthenticationTokenProvider()
};
The CustomOAuthProider contains the following overrides:
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
//We validated that Client Id and redirect Uri are what we expect
if (context.ClientId == "123456" && context.RedirectUri.Contains("localhost"))
{
context.Validated();
}
else
{
context.Rejected();
}
return Task.FromResult<object>(null);
}
public override Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{
var ci = new ClaimsIdentity("Bearer");
context.OwinContext.Authentication.SignIn(ci);
context.RequestCompleted();
return Task.FromResult<object>(null);
}
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId;
string clientSecret;
Guid clientIdGuid;
// Validate the context
context.Validated();
return Task.FromResult<object>(0);
}
Finally, the code in the SimpleAuthenticationTokenProvider is implemented as follows:
public SimpleAuthenticationTokenProvider() : base()
{
this.OnCreate = CreateCode;
this.OnReceive = ReceiveCode;
}
private readonly ConcurrentDictionary<string, string> _authenticationCodes = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
public void CreateCode(AuthenticationTokenCreateContext context)
{
context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
_authenticationCodes[context.Token] = context.SerializeTicket();
}
public void ReceiveCode(AuthenticationTokenReceiveContext context)
{
string value;
_authenticationCodes.TryGetValue(context.Token, out value);
context.DeserializeTicket(value);
}
I have wired up a sample request inside of Postman. When I execute the postman oauth2 Authroization Code grant type the following methods from above fire sequentially:
ValidateClientRedirectUri
AuthorizeEndpoint
ValidateClientAuthentication
ReceiveCode
I am getting back an 'invalid_grant' error from postman, and no authorization code has been returned.
Can anyone point out where I may be going wrong here?
Secondly, how is the callbackurl/redirect url supposed to be used? Is it a fallback to the login page if the user has not authenticated?
UPDATE
I see that a 'code' is coming back in the querystring attached to the redirectURL on the response. Should the redirectURL be a page on the third-party site?
How is this code exchanged for a token?
UPDATE 2
Another question: At what phase do I read/parse the code? I can see the code coming in on the request querystring in my GrantClientCredentials method. Should I just parse it from the querystring and validate? At what point should ReceiveCode be called?

How to implement "user interactive" dependency injection in ASP MVC

I have a full engine that relies on abstractions based on user interactions. This works great with WPF/Xamarin app, cause I can implements this abstractions with window/form.
I have a little problem for porting this engine into ASP MVC.
A simple example can be show as this.
Abstraction interface (simplified)
public interface IQuestionBox
{
Task<bool> ShowYesNoQuestionBox(string message);
}
For WPF, it's really simple, I implement this interface as return the result of a window by calling ShowDialog().
In a simple business class, I can have this kind of calls (simplified) :
public async Task<string> GetValue(IQuestionBox qbox)
{
if(await qbox.ShowYesNoQuestionBox("Question ?"))
{
return "Ok";
}
return "NOk";
}
I really don't see how can I implement this kind of behavior in ASP, due to stateless of HTTP, knowing that this kind of call can be as various as domain/business need. The way I think it should be done is by returning a PartialView to inject into popup, but I don't see how to do this without breaking all the process ...
Anyone has ever done this ?
as I've said, I strongly doesn't recommend this pratice, but its possible, bellow the code that allows to do it, let's go:
To become it's possible I abused the use from TaskCompletionSource, this class allow us to set manually result in a task.
First we need to create a structure to encapsulate the process:
public class Process
{
// this dictionary store the current process running status, you will use it to define the future answer from the user interaction
private static Dictionary<string, Answare> StatusReport = new Dictionary<string, Answare>();
// this property is the secret to allow us wait for the ShowYesNoQuestion call, because til this happen the server doesn't send a response for the client.
TaskCompletionSource<bool> AwaitableResult { get; } = new TaskCompletionSource<bool>(true);
// here we have the question to interact with the user
IQuestionBox QuestionBox { get; set; }
// this method, receive your bussiness logical the receive your question as a parameter
public IQuestionBox Run(Action<IQuestionBox> action)
{
QuestionBox = new QuestionBox(this);
// here we create a task to execute your bussiness logical processment
Task.Factory.StartNew(() =>
{
action(QuestionBox);
});
// and as I said we wait the result from the processment
Task.WaitAll(AwaitableResult.Task);
// and return the question box to show the messages for the users
return QuestionBox;
}
// this method is responsable to register a question to receive future answers, as you can see, we are using our static dictionary to register them
public void RegisterForAnsware(string id)
{
if (StatusReport.ContainsKey(id))
return;
StatusReport.Add(id, new Answare()
{
});
}
// this method will deliver an answer for this correct context based on the id
public Answare GetAnsware(string id)
{
if (!StatusReport.ContainsKey(id))
return Answare.Empty;
return StatusReport[id];
}
// this method Releases the processment
public void Release()
{
AwaitableResult.SetResult(true);
}
// this method end the process delivering the response for the user
public void End(object userResponse)
{
if (!StatusReport.ContainsKey(QuestionBox.Id))
return;
StatusReport[QuestionBox.Id].UserResponse(userResponse);
}
// this method define the answer based on the user interaction, that allows the process continuing from where it left off
public static Task<object> DefineAnsware(string id, bool result)
{
if (!StatusReport.ContainsKey(id))
return Task.FromResult((object)"Success on the operation");
// here I create a taskcompletaionsource to allow get the result of the process, and send for the user, without it would be impossible to do it
TaskCompletionSource<object> completedTask = new TaskCompletionSource<object>();
StatusReport[id] = new Answare(completedTask)
{
HasAnswared = true,
Value = result
};
return completedTask.Task;
}
}
After that the question implementation
public interface IQuestionBox
{
string Id { get; }
Task<bool> ShowYesNoQuestionBox(string question);
HtmlString ShowMessage();
}
class QuestionBox : IQuestionBox
{
Process CurrentProcess { get; set; }
public string Id { get; } = Guid.NewGuid().ToString();
private string Question { get; set; }
public QuestionBox(Process currentProcess)
{
CurrentProcess = currentProcess;
CurrentProcess.RegisterForAnswer(this.Id);
}
public Task<bool> ShowYesNoQuestionBox(string question)
{
Question = question;
CurrentProcess.Release();
return AwaitForAnswer();
}
public HtmlString ShowMessage()
{
HtmlString htm = new HtmlString(
$"<script>showMessage('{Question}', '{Id}');</script>"
);
return htm;
}
private Task<bool> AwaitForAnswer()
{
TaskCompletionSource<bool> awaitableResult = new TaskCompletionSource<bool>(true);
Task.Factory.StartNew(() =>
{
while (true)
{
Thread.Sleep(2000);
var answare = CurrentProcess.GetAnswer(this.Id);
if (!answare.HasAnswered)
continue;
awaitableResult.SetResult(answare.Value);
break;
}
});
return awaitableResult.Task;
}
}
The differences for yours implementaion are:
1 - I create an Identifier to know for who I have to send the aswer, or just to stop the process.
2 - I receive a Process as parameter, because this allows us to call the method
CurrentProcess.Release(); in ShowYesNoQuestion, here in specific, releases the process to send the response responsable to interact with the user.
3 - I create the method AwaitForAnswer, here one more time we use from the TaskCompletionSource class. As you can see in this method we have a loop, this loop is responsable to wait for the user interaction, and til receive a response it doesn't release the process.
4 - I create the method ShowMessage that create a simple html script alert to simulate the user interaction.
Then a simple process class as you should be in your bussiness logical:
public class SaleService
{
public async Task<string> GetValue(IQuestionBox qbox)
{
if (await qbox.ShowYesNoQuestionBox("Do you think Edney is the big guy ?"))
{
return "I knew, Edney is the big guy";
}
return "No I disagree";
}
}
And then the class to represent the user answer
public class Answer
{
// just a sugar to represent empty responses
public static Answer Empty { get; } = new Answer { Value = true, HasAnswered = true };
public Answer()
{
}
// one more time abusing from TaskCompletionSource<object>, because with this guy we are abble to send the result from the process to the user
public Answer(TaskCompletionSource<object> completedTask)
{
CompletedTask = completedTask;
}
private TaskCompletionSource<object> CompletedTask { get; set; }
public bool Value { get; set; }
public bool HasAnswered { get; set; }
// this method as you can see, will set the result and release the task for the user
public void UserResponse(object response)
{
CompletedTask.SetResult(response);
}
}
Now we use all the entire structure create for this:
[HttpPost]
public IActionResult Index(string parametro)
{
// create your process an run it, passing what you want to do
Process process = new Process();
var question = process.Run(async (questionBox) =>
{
// we start the service
SaleService service = new SaleService();
// wait for the result
var result = await service.GetValue(questionBox);
// and close the process with the result from the process
process.End(result);
});
return View(question);
}
// here we have the method that deliver us the user response interaction
[HttpPost]
public async Task<JsonResult> Answer(bool result, string id)
{
// we define the result for an Id on the process
var response = await Process.DefineAnswer(id, result);
// get the response from process.End used bellow
// and return to the user
return Json(response);
}
and in your view
<!-- Use the question as the model page -->
#model InjetandoInteracaoComUsuario.Controllers.IQuestionBox
<form asp-controller="Home" asp-action="Index">
<!-- create a simple form with a simple button to submit the home -->
<input type="submit" name="btnDoSomething" value="All about Edney" />
</form>
<!-- in the scripts section we create the function that we call on the method ShowMessage, remember?-->
<!-- this method request the action answer passing the questionbox id, and the result from a simple confirm -->
<!-- And to finalize, it just show an alert with the process result -->
#section scripts{
<script>
function showMessage(message, id) {
var confirm = window.confirm(message);
$.post("/Home/Answer", { result: confirm, id: id }, function (e) {
alert(e);
})
}
</script>
#Model?.ShowMessage()
}
As I've said, I realy disagree with this pratices, the correct should to write a new dll, to support the web enviroment, but I hope it help you.
I put the project on github to you can download an understand all the solution
I realy hope it can help you
You can create a web socket connection from client side to server side. And work with front-end content with web socket request. It could be implemented as following:
Client side:
$app = {
uiEventsSocket : null,
initUIEventsConnection : function(url) {
//create a web socket connection
if (typeof (WebSocket) !== 'undefined') {
this.uiEventsSocket = new WebSocket(url);
} else if (typeof (MozWebSocket) !== 'undefined') {
this.uiEventsSocket = new MozWebSocket(url);
} else {
console.error('WebSockets unavailable.');
}
//notify if there is an web socket error
this.uiEventsSocket.onerror = function () {
console.error('WebSocket raised error.');
}
this.uiEventsSocket.onopen = function () {
console.log("Connection to " + url + " established");
}
//handling message from server side
this.uiEventsSocket.onmessage = function (msg) {
this._handleMessage(msg.data);
};
},
_handleMessage : function(data){
//the message should be in json format
//the next line fails if it is not
var command = JSON.parse(data);
//here is handling the request to show prompt
if (command.CommandType == 'yesNo') {
var message = command.Message;
var result = confirm(message);
//not sure that bool value will be successfully converted
this.uiEventsSocket.send(result ? "true" : "false");
}
}
}
And init it from ready or load event:
window.onload = function() { $app.initUIEventsConnection(yourUrl); }
Note that you url should begin with ws:// instead of http:// and wss:// instead of https:// (Web Sockets and Web Sockets Secure).
Server side.
Here is a good article for how to setup web sockets at asp.net core application or you could find another one. Note that you should group web socket connections from single user and if you want to send a message to the concrete user, you should send message for every connection from this user.
Every web socket you should accept with AcceptWebSocketAsync() method call and then add instance of this web socket to singleton, which contains a set of web sockets connection groupped by user.
The following class will be used to operate commands:
public class UICommand
{
public string CommandType { get; set; }
public string Message { get; set; }
public Type ReturnType { get; set; }
}
And a full code of singleton for handling sockets
public class WebSocketsSingleton
{
private static WebSocketsSingleton _instance = null;
//here stored web sockets groupped by user
//you could use user Id or another marker to exactly determine the user
private Dictionary<string, List<WebSocket>> _connectedSockets;
//for a thread-safety usage
private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim();
public static WebSocketsSingleton Instance {
get {
if (this._instance == null)
{
this._instance = new WebSocketsSingleton();
}
return this._instance;
}
}
private WebSocketsSingleton()
{
this._connectedSockets = new Dictionary<string, List<WebSocket>>();
}
/// <summary>
/// Adds a socket into the required collection
/// </summary>
public void AddSocket(string userName, WebSocket ws)
{
if (!this._connectedSockets.ContainsKey(userName))
{
Locker.EnterWriteLock();
try
{
this._connectedSockets.Add(userName, new List<WebSocket>());
}
finally
{
Locker.ExitWriteLock();
}
}
Locker.EnterWriteLock();
try
{
this._connectedSockets[userName].Add(ws);
}
finally
{
Locker.ExitWriteLock();
}
}
/// <summary>
/// Sends a UI command to required user
/// </summary>
public async Task<string> SendAsync(string userName, UICommand command)
{
if (this._connectedSockets.ContainsKey(userName))
{
var sendData = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(command));
foreach(var item in this._connectedSockets[userName])
{
try
{
await item.SendAsync(new ArraySegment<byte>(sendData), WebSocketMessageType.Text, true, CancellationToken.None);
}
catch (ObjectDisposedException)
{
//socket removed from front end side
}
}
var buffer = new ArraySegment<byte>(new byte[1024]);
var token = CancellationToken.None;
foreach(var item in this._connectedSockets[userName])
{
await Task.Run(async () => {
var tempResult = await item.ReceiveAsync(buffer, token);
//result received
token = new CancellationToken(true);
});
}
var resultStr = Encoding.Utf8.GetString(buffer.Array);
if (command.ReturnType == typeof(bool))
{
return resultStr.ToLower() == "true";
}
//other methods to convert result into required type
return resultStr;
}
return null;
}
}
Explanation:
on establishing connection from web socket it will be added with
AddSocket method
on sending request to show a message, the required command will be passed into SendAsync method
the command will be serialized to JSON (using Json.Net, however you could serialize in your way) and send to all sockets, related to the required user
after the command sent, application will wait for respond from front end side
the result will be converted to required type and sent back to your IQuestionBox
In the web socket handling your should add some kind of the following code:
app.Use(async (http, next) =>
{
if (http.WebSockets.IsWebSocketRequest)
{
var webSocket = await http.WebSockets.AcceptWebSocketAsync();
var userName = HttpContext.Current.User.Identity.Name;
WebSocketsSingleton.Instance.AddSocket(userName, webSocket);
while(webSocket.State == WebSocketState.Open)
{
//waiting till it is not closed
}
//removing this web socket from the collection
}
});
And your method implementation of ShowYesNoQuestionBox should be kind of following:
public async Task<bool> ShowYesNoQuestionBox(string userName, string text)
{
var command = new UICommand
{
CommandType = "yesNo",
Message = text,
ReturnType = typeof(bool)
};
return await WebSocketsSingleton.Instance.SendAsync(string userName, command);
}
Note that there should be added userName to prevent sending the same message to all of the connected users.
WebSocket should create the persistent connection between server and client sides, so you could simply send commands in two ways.
I am kindly new to Asp.Net Core, so the final implementation could be a bit different from this.
It's actually much the same, except your UI is sort of disconnected and proxied with the HTTP protocol for the most part.
you essentially need to build the same code as your WPF code but then in the browser construct ajax calls in to the controller actions to apply your logic.
To clarify ...
so lets say you are building up a process over a series of questions that based on the users answer you put different steps in to the process.
You can either ...
build the process in the database
build it in session on the server
build it on the client as a js object
then do a post for execution ofthe constructed process.
think of the "statelessness" as a series of short interactions, but the state you keep between them can be done either on the client, in a db or in the users logged in session on the web server.
In your controller you can add an ActionResult that will give you the html response to your jquery modal popup request. Here is an example
public class MController : Controller {
public ActionResult doWork(requirement IQuestionBox)
{
// model is already modelBound/IOC resolved
return PartialView("_doWork", requirement );
}
}
//scripts
$(function(){
$.ajax({
url:"/m/doWork",
type:"get",
success:function(data){
$modal.html(data); // bind to modal
}
});
});
Apologies for not fully understanding the question.
hope this helps!

Categories

Resources