Anonymous Users and the REST API

Anonymous Users and the REST API

This question is answered

We've also posed this question to support, but as it's an urgent issue we wanted to get the community's input as well.  Additionally, I'm sure other users have stumbled across this problem, so if we get an answer from support we'll cross-post it here.

The Requirement: Allow Telligent Community 6.0 to be localized for anonymous users.

The Challenge: This is problematic since, by default, all anonymous users share one user profile ("Anonymous").  Changing the language for this user changes the language for all users. 

The Approach: To mitigate this, we have multiple anonymous users, each with a different language set (e.g., "Anonymous_fr") and each set to IsAnonymous in the cs_Users table.  We have a module that fires on UserKnown to set the CSContext.Current.User property to this localized profile. 

The Problem: This approach works perfectly, except for REST API calls.  REST API calls (such as commenting on a blog post) work fine for "Anonymous", but fail for our other anonymous users (e.g., "Anonymous_fr").  My guess is that this is an issue with the Authorization Code.

Questions:

  • Has anyone used this approach successfully with Telligent Community 6? 
  • Is there an alternate way we should be setting the user profile? 
  • Is there an approach to setting the Authorization-Code/AuthorizationCookie? 
  • Any further insight into anonymous authorization against the REST API?

Note: We do not want to change every factory default widget that performs a REST API call to include the standard authorization headers.  Ideally, we want to piggy back off the same technique used out-of-the-box with the Authorization-Code/AuthorizationCookie.   

 Jeremy

Verified Answer
  • I've done some further digging, and it seems that the AuthorizationCode checks get the current user from HttpContext.Current rather than CSContext.Current.  Whilst your localised anonymous users are sending the correct authorization code, that code is being checked against the standard anonymous user on HttpContext.Current.Ust

    To resolve this issue, make sure to update both CSContext.Current.User and HttpContext.Current.User.  E.g. see the sample below

    (Note, the below example doesn't represent best security or usability practices - you should not allow your customers to directly specify the username to switch to, and you should provide a better way for an end user to switch their language.  It does however provide a simple wholely contained example I've done my testing with locally and was able to successfuly use the REST APIs with)

    public class LocalisedAnonymousUserModule : ICSModule
    {
    	public void Init(CSApplication csa, XmlNode node)
    	{
    		csa.UserKnown += csa_UserKnown;
    	}
    
    	void csa_UserKnown(User originalUser, CSEventArgs e)
    	{
    		//If the user's already logged in, don't do anything
    		if (!originalUser.IsAnonymous)
    			return;
    
    		var newUsername = GetNewAnonUsername();
    		if (String.IsNullOrEmpty(newUsername))
    			return;
    
    		var newUser = Users.GetUser(newUsername);
    		if (!newUser.IsAnonymous)
    			throw new SecurityException("Cannot switch Anonymous user to " + newUsername);
    
    		HttpContext.Current.User = new AnonymousPrinciple(newUser);
    		CSContext.Current.User = newUser;
    	}
    
    	private string GetNewAnonUsername()
    	{
    		// Note, this does NOT represent good security practices.  User input should not directly specify the
    		// user to switch to.  Instead the user should specify a language, which this method translates into
    		// a username
    		var context = HttpContext.Current;
    
    		//Get new username from query string - For a production site, a more user friendly aproach should be used
    		// e.g. custom language selector widget which sets a cookie.
    		string username = context.Request.QueryString["AnonUsername"];
    		if (!String.IsNullOrEmpty(username))
    		{
    			//Persist username for future requests
    			context.Response.SetCookie(new HttpCookie("AnonUsername", username));
    			return username;
    		}
    
    		var anonCookie = context.Request.Cookies["AnonUsername"];
    		return anonCookie == null ? String.Empty : anonCookie.Value;
    	}
    
    	public class AnonymousPrinciple : IPrincipal
    	{
    		private readonly User _user;
    
    		public AnonymousPrinciple(User user)
    		{
    			_user = user;
    			Identity = new AnonymousIdentity(_user.Username);
    		}
    
    		public IIdentity Identity {get; private set;}
    
    		public bool IsInRole(string role)
    		{
    			return _user.IsInRoles(new[] {role});
    		}
    
    		public class AnonymousIdentity : IIdentity
    		{
    			public AnonymousIdentity(string username)
    			{
    				Name = username;
    			}
    
    			public string AuthenticationType { get { return "Anonymous"; } }
    			public bool IsAuthenticated { get { return false; } }
    			public string Name { get; private set; }
    		}
    	}
    }
All Replies
  • I suspect that the way you're changing the user is not changing the value of the AuthorizationCookie to that of the new user.  Can you verify that when you change users, the value of your browser's authorization cookie also changes?

  • Alex - I suspect you're right, and have confirmed it.  What's the best way to set the AuthorizationCookie for a user?  I was looking for a method for retrieving an authorization cookie in the private API, but was having a hard time finding anything.  

  • Can you clarify for me exactly how you're changing the contextual user.  In particular what events / plugins are you using.  (What I'm really wanting to know is *when* in the pipeline you're changing the user)

  • Absolutely.  This is happening in a module that attaches to the UserKnown event.

    public virtual void Init(CSApplication csa, XmlNode node) {
    csa.UserKnown += new CSUserEventHandler(UserKnown);      
    }

    The specific line that sets the Language looks like:

    CSContext.Current.User = Users.GetUser("Anonymous-" + locale); 

    Where "locale" is a string variable representing the user's locale, as detected by the module (e.g., "Anonymous-FR"). 

     

     

  • I'm still looking into this.

    As a temporary work around, can you try not setting the Contextual user if the REST API is being requested.  I.e. if the current url doesn't begin with ~/api.ashx

  • Alex - that was actually the first thing I tried, and oddly it didn't have an affect.  I figured that would allow it to fall back to the standard "Anonymous" user account, which would correctly validate against the Authorization-Code header.  Since I wasn't sure what the variables were, however, I didn't spend too much time verifying that everything was firing correctly.  I'll give that another shot this afternoon and see if I can't work around it.

  • Alex, I've confirmed that bypassing the anonymous user module for the API does not have an effect.  I've additionally confirmed that the AuthorizationCookie (and, thus, Authorization-Code header) do change when the user is changed using this technique.  (Apologies, I should have verified that as part of my original post). 

    In fact, just to be safe, I tried setting the anonymous account using this technique a non-anonymous user.  When doing that, the AuthorizationCookie written for the user was identical to the AuthorizationCookie written if I logged in as that user via the normal method.  Despite that, the post failed when using the module to assign the user.

    I also considered the possibility that the API is authorizing the user before the module is able to assign the correct anonymous user, in which case it may be trying to authorize "Anonymous" based on the Authorization-Code for "Anonymous-FR" (for instance).  This doesn't seem to be the case, however, since event log entries triggered by our module are still firing (I'd assume response processing would stop once authorization failed).  Additionally, I added the Response.Status to the event log entries, and they're coming back as "200 OK", meaning the "403 The request requires an API key" status hasn't yet been set. 

  • I've done some further digging, and it seems that the AuthorizationCode checks get the current user from HttpContext.Current rather than CSContext.Current.  Whilst your localised anonymous users are sending the correct authorization code, that code is being checked against the standard anonymous user on HttpContext.Current.Ust

    To resolve this issue, make sure to update both CSContext.Current.User and HttpContext.Current.User.  E.g. see the sample below

    (Note, the below example doesn't represent best security or usability practices - you should not allow your customers to directly specify the username to switch to, and you should provide a better way for an end user to switch their language.  It does however provide a simple wholely contained example I've done my testing with locally and was able to successfuly use the REST APIs with)

    public class LocalisedAnonymousUserModule : ICSModule
    {
    	public void Init(CSApplication csa, XmlNode node)
    	{
    		csa.UserKnown += csa_UserKnown;
    	}
    
    	void csa_UserKnown(User originalUser, CSEventArgs e)
    	{
    		//If the user's already logged in, don't do anything
    		if (!originalUser.IsAnonymous)
    			return;
    
    		var newUsername = GetNewAnonUsername();
    		if (String.IsNullOrEmpty(newUsername))
    			return;
    
    		var newUser = Users.GetUser(newUsername);
    		if (!newUser.IsAnonymous)
    			throw new SecurityException("Cannot switch Anonymous user to " + newUsername);
    
    		HttpContext.Current.User = new AnonymousPrinciple(newUser);
    		CSContext.Current.User = newUser;
    	}
    
    	private string GetNewAnonUsername()
    	{
    		// Note, this does NOT represent good security practices.  User input should not directly specify the
    		// user to switch to.  Instead the user should specify a language, which this method translates into
    		// a username
    		var context = HttpContext.Current;
    
    		//Get new username from query string - For a production site, a more user friendly aproach should be used
    		// e.g. custom language selector widget which sets a cookie.
    		string username = context.Request.QueryString["AnonUsername"];
    		if (!String.IsNullOrEmpty(username))
    		{
    			//Persist username for future requests
    			context.Response.SetCookie(new HttpCookie("AnonUsername", username));
    			return username;
    		}
    
    		var anonCookie = context.Request.Cookies["AnonUsername"];
    		return anonCookie == null ? String.Empty : anonCookie.Value;
    	}
    
    	public class AnonymousPrinciple : IPrincipal
    	{
    		private readonly User _user;
    
    		public AnonymousPrinciple(User user)
    		{
    			_user = user;
    			Identity = new AnonymousIdentity(_user.Username);
    		}
    
    		public IIdentity Identity {get; private set;}
    
    		public bool IsInRole(string role)
    		{
    			return _user.IsInRoles(new[] {role});
    		}
    
    		public class AnonymousIdentity : IIdentity
    		{
    			public AnonymousIdentity(string username)
    			{
    				Name = username;
    			}
    
    			public string AuthenticationType { get { return "Anonymous"; } }
    			public bool IsAuthenticated { get { return false; } }
    			public string Name { get; private set; }
    		}
    	}
    }
  • Aha!  That explains the behavior perfectly, and is exactly what we need to clear this roadblock.  I appreciate you putting together the example; I've integrated this with our module and it works precisely as expected.  

    Ironically, our original version of this module, for Community Server 2008, wrote exclusively to the HttpContext.Current.  Since it didn't (seem to) have any impact on functionality at that time, we replaced it with the current approach.  Doh!  

    Thanks for your help; it's very much appreciated.