Although the steps may appear lengthy, setting up this demo project is actually quite straightforward. I managed to create it in just about an hour.
I share your sentiments regarding Owin and Katana. Going through that process in the past wasn't the most pleasant experience. Using Firebase made things much simpler.
All of this can be achieved with JWTs!
Upon authentication via Firebase and any social provider, a JSON Web Token (JWT) called firebaseAuthToken
is returned.
Retrieve your Firebase Secret from the Dashboard
JWTs utilize a secret token and a client token. The client token, which is the firebaseAuthToken obtained upon login, works alongside the secret token generated within the Firebase Dashboard.
Store your Firebase Secret in the appSettings section of your Web.config
It's important to store this secret key in the Web.config for easy access later on.
<add key="FirebaseSecret" value="<Enter-Firebase-Secret-Token-Here" />
Create an Action Filter to validate the JWT from the Authorization Header
We can verify the validity of a request by including the client token in the Authorization header. We can also store our secret key received from the Firebase Dashboard on the server side. Upon inspection by Web API, we decode the JWT with the help of a JWT Library (available from NuGet). In case of successful decoding, we check if the token has expired.
public class DecodeJWT: ActionFilterAttribute
{
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
{
string firebaseAuthToken = string.Empty;
if (actionContext.Request.Headers.Authorization != null) {
firebaseAuthToken = actionContext.Request.Headers.Authorization.Scheme;
} else {
throw new HttpException((int) HttpStatusCode.Unauthorized, "Unauthorized");
}
string secretKey = WebConfigurationManager.AppSettings["FirebaseSecret"];
try {
string jsonPayload = JWT.JsonWebToken.Decode(firebaseAuthToken, secretKey);
DecodedToken decodedToken = JsonConvert.DeserializeObject < DecodedToken > (jsonPayload);
// To-do: Check expiry of decoded token
} catch (JWT.SignatureVerificationException jwtEx) {
throw new HttpException((int) HttpStatusCode.Unauthorized, "Unauthorized");
} catch (Exception ex) {
throw new HttpException((int) HttpStatusCode.Unauthorized, "Unauthorized");
}
base.OnActionExecuting(actionContext);
}
}
Create a $httpInterceptor to append the firebaseAuthToken to the header for each request
On the client side, the token must be passed every time. To simplify this process, we create a $httpInterceptor
using Angular, which checks for a firebaseAuthToken
stored in sessionStorage
.
.factory('authInterceptor', function ($rootScope, $q, $window) {
return {
request: function (config) {
config.headers = config.headers || {};
if ($window.sessionStorage.firebaseAuthToken) {
config.headers.Authorization = $window.sessionStorage.firebaseAuthToken;
}
return config;
},
response: function (response) {
if (response.status === 401) {
// To-do: User is not authenticated
}
return response || $q.when(response);
}
};
})
Assign the firebaseAuthToken to sessionStorage upon successful login
Whenever a user logs in, we can assign the value to sessionStorage
.
$rootScope.$on('$firebaseSimpleLogin:login',
function (e, user) {
// Add a cookie for the auth token
if (user) {
$window.sessionStorage.firebaseAuthToken = user.firebaseAuthToken;
}
cb(e, user);
});
Register the DecodeJWT filter globally
In the WebApiConfig.cs
Register method, we can set the DecodeJWT filter to apply to all our ApiControllers.
config.Filters.Add(new DecodeJWT());
Now, whenever a request is made to an ApiController, it will only proceed if a valid JWT is provided. This allows us to save user data to an ApiController after they log in, assuming the data doesn't already exist.
// Globally uses DecodeJWT
public class UsersController: ApiController
{
// POST api/users
public void Post([FromBody] FbUser user) // Refer to GitHub for this Model
{
// Save user if it does not already exist
}
}