Thursday, November 27, 2014

Simple OAuth2 Federated Authentication using DotNetOpenAuth

In my previous entry I’ve blogged on how to implement federated authentication to an external identity provider using the System.IdentityModel subsystem and the WS-Federation. This entry shows how to implement OAuth2 federated authentication (Google, Facebook) with the DotNetOpenAuth library.

Let’s start as usual – a simple web app with two pages, the Default page and the LoginPage. Add an authorization rule and forms authentication redirect to the LoginPage for requests that are not authenticated:

<authentication mode="Forms">
    <forms loginUrl="LoginPage.aspx" />
</authentication>
<authorization>
    <deny users="?" />
</authorization>

From the NuGet package manager, install the DotNetOpenAuth.Ultimate package (a standalone complete DotNetOpenAuth).

The passive OAuth2 (the authorization_code flow) requires the client_id and client_secret parameters (these two kind of authenticate the application at the identity provider side) as well as three Uris: the authentication uri (this is where the browse is redirected to authenticate  users), the token uri (this is where the one-time code returned from the login page is exchanged for an access_token) and the profile uri (which is a part of a graph API and allows the application to retrieve user profile information).

Let’s take Google for example. They have the OpenID Connect Discovery uri where a current information on authentication uris is published. This is where you get the information on all three required uris.

{
 "issuer": "accounts.google.com",
 "authorization_endpoint": "https://accounts.google.com/o/oauth2/auth",
 "token_endpoint": "https://accounts.google.com/o/oauth2/token",
 "userinfo_endpoint": "https://www.googleapis.com/plus/v1/people/me/openIdConnect",
 "revocation_endpoint": "https://accounts.google.com/o/oauth2/revoke",
 "jwks_uri": "https://www.googleapis.com/oauth2/v2/certs",
 "response_types_supported": [
  "code",
  "token",
  "id_token",
  "code token",
  "code id_token",
  "token id_token",
  "code token id_token",
  "none"
 ],
 "subject_types_supported": [
  "public"
 ],
 "id_token_alg_values_supported": [
  "RS256"
 ],
 "token_endpoint_auth_methods_supported": [
  "client_secret_post"
 ]
}

To get client_id and client_secret you need to register your application at the Google Console projects page. You need to create a new project, go to APIs&auth/Credentials, register a redirect uri (a uri in your app Google should redirect back to) and they generate both the client_id and client_secret.

Also remember to go to APIs&auth/APIs and switch Google+ API to ON (or your profile API calls will end up with 403)!

Be warned that some details of OAuth2 Google authentication have changed lately and are subjects for further changes.

When you are ready, go back to Visual Studio.

We are going to use the WebServerClient class which is designed to handle the OAuth2 authorization_code flow. We inherit from it to provide all Google endpoints:

public class GoogleClient : WebServerClient
{
    private static readonly AuthorizationServerDescription GoogleDescription =
        new AuthorizationServerDescription
        {
            TokenEndpoint         = new Uri( "https://accounts.google.com/o/oauth2/token" ),
            AuthorizationEndpoint = new Uri( "https://accounts.google.com/o/oauth2/auth" ),
            ProtocolVersion       = ProtocolVersion.V20,                
        };
 
    public const string ProfileEndpoint = "https://www.googleapis.com/plus/v1/people/me/openIdConnect";
 
    public const string OpenId       = "openid";
    public const string ProfileScope = "profile";
    public const string EmailScope   = "email";
 
    public GoogleClient()
        : base( GoogleDescription )
    {
    }
}

We also need a helper class to deserialize JSON profile information

public class GoogleProfileAPI
{
    public string email { get; set; }
    public string given_name { get; set; }
    public string family_name { get; set; }
 
    private static DataContractJsonSerializer jsonSerializer =
        new DataContractJsonSerializer( typeof( GoogleProfileAPI ) );
 
    public static GoogleProfileAPI Deserialize( Stream jsonStream )
    {
        try
        {
            if ( jsonStream == null )
            {
                throw new ArgumentNullException( "jsonStream" );
            }
 
            return (GoogleProfileAPI)jsonSerializer.ReadObject( jsonStream );
        }
        catch ( Exception ex )
        {
            return new GoogleProfileAPI();
        }
    }
}

and a technical helper class to strip off unnecessary query string parameters from the web API calls

public class MyAuthorizationTracker : IClientAuthorizationTracker
{
 
    public IAuthorizationState GetAuthorizationState( 
        Uri callbackUrl, 
        string clientState )
    {
        return new AuthorizationState
        {
            Callback = new Uri( callbackUrl.GetLeftPart( UriPartial.Path ) ) 
        };
    }
 
}

Actual OAuth2 flow code is straightforward now

protected void Page_Load( object sender, EventArgs e )
 {
     IAuthorizationState authorization = gClient.ProcessUserAuthorization();
     // Is this a response from the Identity Provider
     if ( authorization == null )
     {
         // no
         
         // Google will redirect back here
         Uri uri = new Uri( "http://localhost:62889/LoginPage.aspx" );
         // Kick off authorization request with OAuth2 scopes
         gClient.RequestUserAuthorization( returnTo: uri, 
             scope: new[] { GoogleClient.OpenId, GoogleClient.ProfileScope, GoogleClient.EmailScope } );
     }
     else
     {
         // yes
 
         var request = WebRequest.Create( GoogleClient.ProfileEndpoint );
         // add an OAuth2 authorization header
         // if you get 403 here, turn ON Google+ API on your app settings page
         request.Headers.Add( 
              HttpRequestHeader.Authorization, 
              string.Format( "Bearer {0}", Uri.EscapeDataString( authorization.AccessToken ) ) );
         // Go to the profile API
         using ( var response = request.GetResponse() )
         {
             using ( var responseStream = response.GetResponseStream() )
             {
                 var profile = GoogleProfileAPI.Deserialize( responseStream );
                 if ( profile != null &&
                     !string.IsNullOrEmpty( profile.email ) )
                     FormsAuthentication.RedirectFromLoginPage( profile.email, false );
             }
         }
     }
 }

The Google Client is just

// replace with actual values!
public readonly GoogleClient gClient = new GoogleClient
{
    AuthorizationTracker       = new MyAuthorizationTracker(),
    ClientIdentifier           = "my client id",
    ClientCredentialApplicator = ClientCredentialApplicator.PostParameter( "my client secret" )
};

Monday, November 24, 2014

Simplest SAML1.1 Federated Authentication

Yet another entry on Federated Authentication. This time we focus on the Relying Party side and our goal is to federate with an STS with as little effort as possible and as much code as possible, no unnecessary configuration sections in web.config. The STS could be any SAML1.1 compliant STS: the ADFS2, the Azure Federation Service, Thinktecture Identity Server.

Last time I’ve blogged on the SessionAuthenticationModule and how it is used to create and then retain a session of authenticated user. The WS-FAM (or FAM; WS-Federated Authentication Module) will be used to redirect the flow to the STS and read the response.

Fortunately, the FAM is also integrated in .NET 4.5. I won’t need it in my web.config, however, the SAM won’t work if there is nothing in the configuration file. Make sure to refer to the previous blog entry to get all the details about configuring SAM in web.config.

Assuming you have your login resource (a login page), an example code to integrate with an STS would be as follows. The implementation has two conditional branches:

  • one for the very first GET of the login resource – this is where we build a WS-Federation compliant request and go to the STS for authentication
  • one for the STS response – this is where we get the SAML token out of the response and validate it for signature validity and the certificate acceptance
public partial class LoginPage : System.Web.UI.Page
{
    // a web forms example but MVC version would be the same
    protected void Page_Load( object sender, EventArgs e )
    {
        // sam is configured in web.config
        var sam = FederatedAuthentication.SessionAuthenticationModule;
 
        // fam is not
        var fam                     = new WSFederationAuthenticationModule();
        fam.FederationConfiguration = FederatedAuthentication.FederationConfiguration;
 
        var request = new HttpContextWrapper( this.Context ).Request;
 
        // is this the response from the STS
        if ( !fam.IsSignInResponse( request ) )
        {
            // no
 
            // the STS
            fam.Issuer = "https://your.sts.address/here";
            // the return address
            fam.Realm  = this.Request.Url.AbsoluteUri;
 
            var req      = fam.CreateSignInRequest( string.Empty, null, false );
 
            // go to STS
            Response.Redirect( req.WriteQueryString() );
        }
        else
        {
            // yes
 
            // get the SAML token
            var securityToken = fam.GetSecurityToken( request );
 
            var config = new SecurityTokenHandlerConfiguration
            {
                CertificateValidator = X509CertificateValidator.None,
                IssuerNameRegistry   = new CustomIssuerNameRegistry()
            };
            config.AudienceRestriction.AudienceMode = AudienceUriMode.Never;
 
            var tokenHandler = new SamlSecurityTokenHandler
            {
                CertificateValidator = X509CertificateValidator.None,
                Configuration        = config
            };
 
            // validate the token and get the ClaimsIdentity out of it
            var identity  = tokenHandler.ValidateToken( securityToken );
 
            var principal = new ClaimsPrincipal( identity );
 
            var token = sam.CreateSessionSecurityToken( principal, string.Empty,
                  DateTime.Now.ToUniversalTime(), DateTime.Now.AddMinutes( 20 ).ToUniversalTime(), false );
 
            sam.WriteSessionTokenToCookie( token );
 
            this.Response.Redirect( this.Request.QueryString["ReturnUrl"] );
        }
    }
 
 
    // a simple helper to get the absolute uri of a local uri
    private Uri GetAbsoluteUri( string redirectUrl )
    {
        var redirectUri = new Uri( redirectUrl, UriKind.RelativeOrAbsolute );
 
        if ( !redirectUri.IsAbsoluteUri )
        {
            redirectUri = 
               new Uri( new Uri( Request.Url.GetLeftPart( UriPartial.Authority ) + Request.ApplicationPath ), redirectUri );
        }
 
        return redirectUri;
    }
}

We also need a custom issuer registry (referenced in the code above) to make sure we accept the certificate. An example registry below accepts any certificate, make sure you customize it to look into a concrete registry:

public class CustomIssuerNameRegistry : IssuerNameRegistry
{
    public override string GetIssuerName( SecurityToken securityToken )
    {
        X509SecurityToken x509Token = securityToken as X509SecurityToken;
 
        return x509Token.Certificate.Subject;
    }
}

I really like the simplcity of the code and the fact it is so concise. As I always do, I strongly encourage to read the handbook on Claims-Based Identity and Access Control.