Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Bendik Engebretsen 105 posts 202 karma points
    Mar 16, 2017 @ 13:06
    Bendik Engebretsen
    0

    Securing a REST API using JWT and Umbraco front-end members

    I have an Umbraco project with a REST API that delivers data from the Umbraco content database. The content is protected in the sense that users need a valid subscription to access it. To do this, I have used the built-in membership provider in Umbraco and built a surface with views and controller where the user can log in and manage his/her account.

    Now, this content is also going to be available through a mobile app. For mobile apps, the cookie/session based authentication is not a good option, because it will require the user to log in frequently when the session/cookie expires. This is not a good user experience on a mobile app. So I decided to try and implement a stateless authentication based on JWT, JSON Web Tokens. But this turned out to be quite a journey with a steep learning curve. So I thought I'd share some of my experiences and code:-)

    Creating a REST API

    To create your own REST API in Umbraco, simply create a Controller in your project and then change the base class to UmbracoApiController.

    public class MyApiController : UmbracoApiController
    {
        [HttpGet]
        public string GetSomeContent()
        {
            IContentService cs = ApplicationContext.Services.ContentService;
            // Pull out some content from Umbraco here
            return someContent;
        }
    }
    

    Now, your api methods can be reached through this url scheme: /Umbraco/api/< api name >/< method >, so in this case /Umbraco/api/MyApi/GetSomeContent.

    Securing your API

    There is of course the option of going the OAuth route in your Umbraco project, in which case a lot of the authentication and security code will be quite straightforward to implement in a standardised way. But what if you - like me - are stuck with the standard Umbraco member API and protection mechanisms? Then, as we will see, things get a little more complicated.

    Testing tool

    The first thing you should get in place is a tool for testing your REST API. I have chosen Swagger, which has a nice YAML editor and an easy UI for sending API requests to your server. And, if you properly describe your API in YAML, you'll also end up having a full documentation of the API so that other developers can easily understand it and use it:-) We'll look at some YAML later on.

    JWT - JSON Web Token

    You should read up a little bit on the basics of this technology: About JWT at the AuthO Website To put it short, a JWT is an encrypted string consisting of three parts: header, payload and signature. And the payload is the interesting part, it is where we specify our claims and metadata. The header specifies the format and encryption method of the package, and the signature is an encrypted string that verifies the package is genuine and issued by our server. The encryption uses a secret which must be stored safely in our server, no one else must know about this. Then everything is encoded as Base64 strings and the three parts are separated by dots.

    Refresh Tokens and Access Tokens

    I'll jump right to the conclusion of why we're using two types of tokens: To avoid passing user information and look up in the member database for every protected API call. The procedure goes like this:

    • The user authenticates himself with username and password.
    • The server then creates a Refresh Token which contains the username, and store this in the member database.
    • This token is now returned to the client app. The client app stores this token safely, and typically on a mobile app, it never expires, because you don't want the user having to log in more than once.
    • The client app uses the Refresh Token to request an Access Token. This token does not contain the username, but rather an expiration time.
    • The Access Token is used to access protected methods in the API. The server decodes the token, checks the signature and expiry time and proceeds to return data in the response if everything is ok.
    • If the signature is wrong, it returns a 401 Unauthorized with a message "Invalid Access Token".
    • If it has expired, it also returns a 401, but with a message "Access Token Expired". The client will then need to use the Refresh Token to request a new Access Token to be able to use the API. The server checks the Refresh Token - which contains the username - against the token stored in the member database, and if its ok, returns a new Access Token.

    Integration in Umbraco

    The first thing we need is a good package for handling the JWT format itself. There should be no need to reinvent the wheel here, and I found a good NuGet which does all the encoding and decoding work, it's simply called JWT. Having installed that, let's look at some code (paste this into your Controller class):

        private const string jwtSecret1 = "< Your 40 char secret >";
        private const string jwtSecret2 = "< Another 40 char secret >";
    
        private class JwtRefreshTokenPayload
        {
            public string sub { get; set; }
            public string username { get; set; }
        }
    
        private class JwtAccessTokenPayload
        {
            public string sub { get; set; }
            public DateTime expires { get; set; }
        }
    
        [HttpPost]
        public HttpResponseMessage Authenticate(string username, string password)
        {
            JwtRefreshTokenPayload payload = new JwtRefreshTokenPayload() {
                sub = "MyRefreshToken",
                username = username
            };
    
            if (Members.Login(username, password))
            {
                var ms = Services.MemberService;
                var member = ms.GetByUsername(username);
                string refreshToken = JsonWebToken.Encode(payload, jwtSecret1, JwtHashAlgorithm.HS256);
                // Save refresh token on member. It must be provided by the client to get access tokens for the API.
                member.Properties["refreshToken"].Value = refreshToken;
                ms.Save(member);
    
                return Request.CreateResponse<string>(HttpStatusCode.OK, refreshToken);
            }
    
            return Request.CreateResponse<string>(HttpStatusCode.Unauthorized, "Invalid Username and Password");
        }
    
        [HttpGet]
        public HttpResponseMessage GetAccessToken()
        {
            try
            {
                string refreshToken = Request.Headers.GetValues("refreshToken").FirstOrDefault();
                DefaultJsonSerializer jsonSerializer = new DefaultJsonSerializer();
                JwtRefreshTokenPayload payloadR = jsonSerializer.Deserialize<JwtRefreshTokenPayload>(JsonWebToken.Decode(refreshToken, jwtSecret1));
    
                if (payloadR.sub == "MyRefreshToken")
                {
                    var ms = Services.MemberService;
                    var member = ms.GetByUsername(payloadR.username);
    
                    if (member != null)
                    {
                        string storedRefreshToken = (string)member.Properties["refreshToken"].Value;
                        if (refreshToken == storedRefreshToken)
                        {
                            JwtAccessTokenPayload payloadA = new JwtAccessTokenPayload()
                            {
                                sub = "MyAccessToken",
                                expires = DateTime.Now.AddHours(20)
                            };
    
                            string accessToken = JsonWebToken.Encode(payloadA, jwtSecret2, JwtHashAlgorithm.HS256);
                            return Request.CreateResponse<string>(HttpStatusCode.OK, accessToken);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
            }
    
            return Request.CreateResponse<string>(HttpStatusCode.Unauthorized, "Invalid Refresh Token");
        }
    
        private string VerifyAccessToken()
        {
            try
            {
                string accessToken = Request.Headers.GetValues("accessToken").FirstOrDefault();
                DefaultJsonSerializer jsonSerializer = new DefaultJsonSerializer();
                string jsonToken = JsonWebToken.Decode(accessToken, jwtSecret2);
                JwtAccessTokenPayload payload = jsonSerializer.Deserialize<JwtAccessTokenPayload>(jsonToken);
                if (payload.sub != "MyAccessToken")
                    return "Invalid Access Token";
                if (payload.expires > DateTime.Now)
                    return "OK";
                return "Access Token Expired";
            }
            catch (Exception ex)
            {
                return "Invalid Access Token";
            }
        }
    
        [HttpGet]
        public HttpResponseMessage GetSomeContent()
        {
            string verifyResult = VerifyAccessToken();
            if (verifyResult != "OK")
                return Request.CreateResponse<string>(HttpStatusCode.Unauthorized, verifyResult);
            IContentService cs = ApplicationContext.Services.ContentService;
            // Pull out some content from Umbraco here
            return Request.CreateResponse<string>(HttpStatusCode.OK, someContent);
        }
    

    The /Authenticate POST is sent with Username and Password in the query string to get a Refresh Token for the user, provided the user is a member in the Umbraco database. The Refresh Token is stored with the user in the Umbraco database.

    The /GetAccessToken GET is sent with the Refresh Token in a custom header called "refreshToken". If the Refresh Token is verified, it returns an Access Token valid for 20 hours.

    The /GetSomeContent GET is sent with the Access Token in a custom header called "accessToken". If the Access Token is verified, the method returns its data in the response.

    To test this in Swagger, you can use the following YAML:

    swagger: '2.0'
    
    info:
      version: "0.0.1"
      title: My Umbraco API
    
    host: localhost:XXXXX
    
    schemes:
      - http
    
    basePath: /Umbraco/api/MyApi
    
    produces:
      - application/json
    
    paths:
      /Authenticate:
        post:
          summary: My API Authentication
          description: Authenticate member for API access
          parameters:
            - name: username
              in: query
              description: Username
              required: true
              type: string
            - name: password
              in: query
              description: Password
              required: true
              type: string
          responses:
            200:
              description: Refresh token (JWT)
              schema:
                type: string
            401:
              description: Unauthorized error
              schema:
                type: string
            default:
              description: Unexpected error
              schema:
                $ref: '#/definitions/Error'
      /GetAccessToken:
        get:
          summary: My API Access Token
          description: Get an access token for My Umbraco API endpoints
          parameters:
            - name: refreshToken
              in: header
              description: Refresh token
              required: true
              type: string
          responses:
            200:
              description: Access token (JWT)
              schema:
                type: string
            401:
              description: Unauthorized error
              schema:
                type: string
            default:
              description: Unexpected error
              schema:
                $ref: '#/definitions/Error'
      /GetSomeContent:
        get:
          summary: Test Method
          description: Test My Umbraco API
          parameters:
            - name: accessToken
              in: header
              description: Access token
              required: true
              type: string
          responses:
            200:
              description: Some content
              schema:
                type: string
    definitions:
      Error:
        type: object
        properties:
          code:
            type: integer
            format: int32
          message:
            type: string
    

    AND, there is one more thing before you'll be able to test: You must enable CORS (Cross-Origin Resource Sharing) for your API, so that Swagger (or your mobile app!) can access it via its own server. In my scenario, I needed to do two things: enable CORS in web.config:

      <system.webServer>
        <httpProtocol>
          <customHeaders>
            <add name="Access-Control-Allow-Origin" value="*" />
            <add name="Access-Control-Allow-Headers" value="refreshToken,accessToken" />
          </customHeaders>
        </httpProtocol>
      </system.webServer>
    

    Then enable OPTIONS preflight request for all your API methods, so that they accept the requests through CORS. I chose to do this generally in Global.asax:

    public class Global : UmbracoApplication
    {
        //... other global.asax methods, f.ex. override OnApplicationStartup()
    
        private void Application_BeginRequest(object sender, EventArgs e)
        {
            try
            {
                if (Request.Headers.AllKeys.Contains("Origin") && Request.HttpMethod == "OPTIONS")
                {
                    Response.Flush();
                }
            }
            catch { }
        }
    }
    

    Testing

    Try /Authenticate with a valid Username and Password, and copy the JWT in the response. Paste it into the "refreshToken" parameter of /GetAccessToken and invoke. Copy the new JWT from this method into the "accessToken" parameter of the /GetSomeContent method and invoke. You should get someContent in the response. Try also tampering with the JWTs and see that you get 401s.

    Final notice

    Needless to say, this whole scheme should be run over https. Otherwise, someone can easily sniff out usernames, passwords and tokens from your requests.

    And feel free to comment on my technique, this is my first time for almost everything covered here. If I'm lucky, someone with greater experience than me can point out any flaws (which I'm quite sure there must be)...

  • Micha Somers 134 posts 597 karma points
    Mar 16, 2017 @ 14:18
    Micha Somers
    0

    Hi Bendik,

    Thanks for sharing this in such details ... really appreciated!

    A question that pops up about the Refresh token:

    What is the added value of a Refresh token compared to a username/pw combination?

    Having a valid Refresh token, you will always have the ability to create a new Access token which will give access to the protected areas. Although handy, it also sounds vurnerable for abuse by eg. hackers or thieves.

    Did you implement something for situations where a password is being changed on the server?

    If not, your Refresh token can still be used without any restrictions (even when the password has been changed) and would be less secure than using username/pw.

    Best regards, Micha

  • Bendik Engebretsen 105 posts 202 karma points
    Mar 20, 2017 @ 10:43
    Bendik Engebretsen
    0

    Hi Micha

    Thanks your feedback! You have some good points here. The concept of a refresh token I got from this article from Auth0. As far as I understand it, the basic idea is to avoid passing the password every time you want an access token. This minimizes the window for compromising the username/password combination, as it is only passed once (usually, for a mobile app). (Note: I'm not using the Auth0 implementation, I have rolled my own to get everything fitted within the existing Umbraco framework.)

    Now, regarding a password change, you are right that this must be handled: The user must log in again and the client app must obtain a new refresh token. I have now solved this by adding a time stamp to the refresh token, and upon getting an access token, this is checked against the LastPasswordChangeDate in the Umbraco member database. So, the code now looks like this (fragments):

    private class JwtRefreshTokenPayload
    {
        public string sub { get; set; }
        public string username { get; set; }
        public DateTime issued { get; set; }
    }
    

    ...

        public HttpResponseMessage Authenticate(string username, string password)
        {
            JwtRefreshTokenPayload payload = new JwtRefreshTokenPayload() {
                sub = "MyRefreshToken",
                username = username,
                issued = DateTime.Now
            };
    

    ...

                        string storedRefreshToken = (string)member.Properties["refreshToken"].Value;
                        if (refreshToken == storedRefreshToken)
                        {
                            // Check if password has been changed. If so, user must log in again and client gets a new refresh token.
                            if (member.LastPasswordChangeDate > payloadR.issued.ToLocalTime())
                            {
                                return Request.CreateResponse<string>(HttpStatusCode.Unauthorized, "Password Changed");
                            }
    

    I really appreciate your thoughts on this, Micha, so feel free to comment!

  • Salahuddin Khan 2 posts 71 karma points
    Apr 05, 2017 @ 07:34
    Salahuddin Khan
    0

    Hi Bendik Engebretsen

    Thanks for sharing your experience, I have read your post and found it very useful.

    I have a question in mind, that Will it be considered as a good practice to use Umbraco Identity with Refresh token to secure an Umbraco REST API for umbraco front-end members?

    Custom UmbracoIdentityStartup ConfigureMiddleware method might look like

    protected override void ConfigureMiddleware(IAppBuilder app)
    {
                    //Configure the application for OAuth based flow
                    var PublicClientId = "self";
                    var OAuthOptions = new OAuthAuthorizationServerOptions
                    {
                        TokenEndpointPath = new PathString("/Token"),
                        Provider = new ApplicationOAuthProvider(PublicClientId),
                        AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
                        AccessTokenExpireTimeSpan = TimeSpan.FromDays(30),
                        AuthorizationCodeExpireTimeSpan = TimeSpan.FromHours(3),
                        RefreshTokenProvider = new ApplicationRefreshTokenProvider(),
                        AccessTokenFormat = new ApplicationJwtFormat(ConfigurationManager.AppSettings["AuthURL"]),
                        AllowInsecureHttp = false
                    };
    
                    //Enable the application to use bearer tokens to authenticate users
                    app.UseOAuthBearerTokens(OAuthOptions);
    
                    //Ensure owin is configured for Umbraco back office authentication. If you have any front-end OWIN
                    // cookie configuration, this must be declared after it.
                    app
                       .UseUmbracoBackOfficeCookieAuthentication(ApplicationContext, PipelineStage.Authenticate)
                       .UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext, PipelineStage.Authenticate);
    
                    // Enable the application to use a cookie to store information for the 
                    // signed in user and to use a cookie to temporarily store information 
                    // about a user logging in with a third party login provider 
                    // Configure the sign in cookie
                    app.UseCookieAuthentication(
                        //You can modify these options for any customizations you'd like
                        new FrontEndCookieAuthenticationOptions(),
                        PipelineStage.Authenticate);
    
                    // Uncomment the following lines to enable logging in with third party login providers
    
                    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
    }
    

    Best Regards, Salahuddin

  • Bendik Engebretsen 105 posts 202 karma points
    May 03, 2017 @ 16:37
    Bendik Engebretsen
    0

    I haven't used the Umbraco Identity plugin, so I'm unable to give you an answer. But I'm quite sure the author, Shannon Deminick could give you an answer if you post a question on the github project.

  • Biagio Paruolo 1593 posts 1824 karma points c-trib
    Aug 22, 2017 @ 14:21
    Biagio Paruolo
    1

    I suggest this package: https://github.com/mattbrailsford/umbraco-authu more simple to use then Umbraco Identity plugin that authenticate only backend users.

Please Sign in or register to post replies

Write your reply to:

Draft