Search code examples
c#asp.netoauth-2.0postmanopenid-connect

how to implement OpenID Connect from a private provider in the c# asp.net


I have an ASP.NET MVC application that needs to integrate OpenID Connect authentication from a Private OpenID Connect (OIDC) Provider, and the flow has the following steps:

  1. user click sign-in

  2. it will redirect the user to the private OIDC site for authentication using the below HTTP GET request: enter image description here

  3. after successful login in the private OIDC site, it will redirect back to my site and get the uri with a code result showing as below: enter image description here

  4. then i will need to use the code from the above and make an HTTP POST call to the private ODIC token endpoint to get the access token for this user.

So, my questions #1 is: how to implement this in the c# asp.net app?

Also, I tried this in Postman "Get New Access Token", and I got the token.

as you can see after I supply all the parameters and click Request Token, it popup the login winnow, enter image description here after successful sign in ,it shows the token enter image description here

my questions #2 is: similar to question #1, is there anyway to implement this in c# asp.net app? like in a asp.net mvc app, add a link-button with the url in the 1st image, when user clicks it will redirect it back to myapp with the code, and then use this code to make HTTP POST call in the stpe3.


Solution

  • You can find an open source example of this on GitHub. The license of that is very permissive, and it's well documented. I've used it in various workshops and trainings, so most of the bugs have been worked out. I would advise you to dig into that. For completeness though, I'll describe the general process here, and use that as the basis for explaining.

    Any Web application implementing the OpenID Connect code flow will include two parts:

    1. The start of the flow and
    2. The handling of the callback

    The application that perform these two things is called a "client" or "relying party". The thing that this client communicates with using the OpenID Connect protocol is called an OpenID Connect Provider (OP) and is often also referred to as an Identity Provider (IdP).

    The first part of the client implementation will show a view that contains a button. This button will be the typical "login" or "sign in" button. Note that this is optional, and the application may immediately redirect the user to the OP if it detects that the user doesn't have a session. Given your question above, however, this won't be the case for you, and the client will start by rendering a view that shows such a button. The view might look something like this:

    <div>
        @if(Session.Count == 0) {
            <p>
                This is a demo application to demonstrate the use for OAuth2 
                and OpenID Connect. 
            </p>
    
            <p>
                Pressing Sign In will redirect you to @ViewData["server_name"] 
                and authorize the application to access your profile info. The 
                data will only be used to demonstrate the possibilities of the 
                OpenID Connect protocol and will not be stored. Be sure to 
                revoke access when you are satisfied.
            </p>
            <div>
                <a href="/login">Sign In</a>
            </div>
        } else {
          // ...
        }
    </div>
    
    

    This view would be rendered by a very basic controller that is wired up in the routing configuration established in Global.asax.cs. When the sign in button is clicked, the OpenID Connect parts start. The controller that handles this request would simply redirect to the OP's authorization endpoint. This might look like this, in the most basic case:

    public class LoginController : Controller
    {
        private static string start_oauth_endpoint = Helpers.Client.Instance.GetAuthnReqUrl();
    
        public ActionResult Index()
        {
            return Redirect(start_oauth_endpoint);
        }
    }
    

    The interesting part is how the authorization endpoint is obtained. This could be hard-coded, defined in Web.config, or obtained from the metadata of the OP. In the example I referenced above, it fetches the OP's metadata on app start. This is done in AppConfig located in the App_Start directory of the Web app. This performs an HTTP GET request to the issuer ID (located in Web.config) with /.well-known/openid-configuration). The reason for fetching this metadata on app start rather than putting all of it in configuration is to reduce the coupling of the OP and client.

    The redirection performed in the snipped above will have a few important query string parameters. Some of these will be known at design-time, and will be hard coded. Others will be configured in Web.config. Some will be dynamically computed at run-time. These are listed below:

    client_id
    The client ID of this MVC Web app.
    response_type
    The response type the OP should use. This will always be code in your case.
    scope
    The scope of access that the client is requesting. This will include at least openid.
    redirect_uri
    The redirect URI where the OP should send the user to after they authenticate and authorize the client.

    Other request parameters can also be sent. To help you figure out which to send, and the effect they have on the flow, checkout oauth.tools. This is like "Postman for OAuth and OpenID Connect". It's fantastic; you'll love it. There, you can form all sorts of OAuth and OpenID Connect flows with their various parameters.

    Once this redirect is made to the OP, the user will authenticate. The user may also have to consent to the client's access to their protected resources. In any event, the OP will redirect the user to the callback after that. This is the second part of the implementation.

    Here, we'll have a CallbackController (or something along those lines). It will look like this (in its simplest form):

    public class CallbackController : Controller
    {
        public ActionResult Index()
        {
            try
            {
                string responseString = Helpers.Client.Instance
                    .GetToken(Request.QueryString["code"]);
    
                SaveDataToSession(responseString);
            }
            catch (Exception e)
            {
                Session["error"] = e.Message;
            }
    
            return Redirect("/");
        }
    }
    

    The important part of this snippet is that it's obtaining the code from the query string, and making an HTTP POST request to the OP's token endpoint (which was also located by parsing the OP's metadata). If this succeeds, it will save the response in the session for later use. The GetToken method will look something like this:

    public String GetToken(String code)
    {
        var values = new Dictionary<string, string>
        {
            { "grant_type", "authorization_code" },
            { "client_id", client_id},
            { "client_secret", client_secret },
            { "code" , code },
            { "redirect_uri", redirect_uri}
        };
    
    
        HttpClient tokenClient = new HttpClient();
        var content = new FormUrlEncodedContent(values);
        var response = tokenClient.PostAsync(token_endpoint, content).Result;
    
        if (response.IsSuccessStatusCode)
        {
            var responseContent = response.Content;
    
            return responseContent.ReadAsStringAsync().Result;
        }
    
        throw new OAuthClientException("Token request failed with status code: " + response.StatusCode);
    }
    

    This will send the code to the OP and get an access token, ID token, and perhaps a refresh token back in exchange. The important parts of this code are:

    • The contents are form URL-encoded not JSON. This is a common mistake.
    • The same redirect URI that was sent previously is included again. This is to match up the two requests at the OP.
    • The grant_Type is alway authorization_code.
    • The client authenticates somehow. In this case, by including the same client_id in the request as was previously sent together with a secret in the client_secret form element.
    • The HTTP method used (as I said above) is a POST, not a GET. This too is a common mistake.

    In my example above, I redirect back to the default, HomeController. Now, that if statement's else condition executes. In this, it can find the tokens:

    <div>
        @if(Session.Count == 0) {
            // ...
        } else {
            @if(Session["id_token"] != null) {
                <div>
                    ID Token:<br>
                    <pre>@Session["id_token"]</pre>
                </div>
            }
    
            @if(Session["access_token"] != null) {            
                <div>
                    Access Token:<br>            
                    <pre>@Session["access_token"]</pre>                
                </div>
            }
    
            @if(Session["refresh_token"] != null) {
                <div>
                    Refresh Token:<br>                
                    <pre>@Session["refresh_token"]</pre>
                </div>
            }
        }
    </div>
    

    The example is more elaborate than this, but it hopefully gives you an idea. Go through that, check the README, and have fun learning more about OpenID Connect!