ASP.Net Identity 2 - custom response from OAuthAuthorizationServerProvider
Asked Answered
N

2

5

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.

Nimiety answered 15/4, 2016 at 12:39 Comment(0)
K
5

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://mcmap.net/q/1346140/-web-api-return-oauth-token-as-xml

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
//...
Klinges answered 20/4, 2016 at 21:35 Comment(5)
thank You for Your solution. I'll try it right away. Sadly OAuthAuthorizationServerProvider isn't providing any simple solution for this. I think I'm not the only one who would like to do this kind of mix authentication.Nimiety
I've tried it and unfortunately it isn't working. I've set up break point and I can confirm that debugger accessed code inside c.Get<bool>("sms_grant:sent") but response is unchanged - I still get same response as before. Could You please try Your code? Maybe it just needs small fixNimiety
I've searched a bit and found this answer by @Lazy Coder https://mcmap.net/q/1346140/-web-api-return-oauth-token-as-xml his solution required clearing response body using MemoryStream. I've build POC solution. I'll post it in here below my question, please take a look at it. Maybe there is something worth fixing. Also please fix Your answer. I'd like to award Your answer, because it pointed me to right track, but at same time I'd like Your answer to be complete working solution.Nimiety
Great findings! I'm glad I was able to help you. I've edited my answer and fixed it following Lazy Coder answer.Klinges
Thank You one more time for help and pointing me in right direction! Maybe there is simpler way of doing this, but for now this is working just fine.Nimiety
S
1

I would recommend to have a look at this answer :

https://mcmap.net/q/491917/-aspnet-identity-2-customize-oauth-endpoint-response

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);
}
Seedy answered 12/7, 2018 at 8:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.