2

I am using MVC 5. The problem is that after SSO redirects back to the app after authentication the login method returnUrl drops the applicaitonId querystring parameter. Please help!

Here is the flow.

  1. The app redirects unauthorized users to a login method, preserving the original request in the returnUrl.

    The original request is

     http://localhost:25451/shared/download?documentGroup=133&applicationId=3153
    

    the returnUrl is

     /shared/download?documentGroup=133&applicationId=3153
    
  2. The app redirects to a SSO CAS server, sending along the HttpUtility.Encode returnUrl as a parameter along with login Url both part of the service parameters.

     https://{redacted}/cas/login?service=http://localhost:25451/account/login%3freturnUrl%3d%2fshared%2fdownload%3fdocumentGroup%3d133%26applicationId%3d3153
    
  3. After authentication, the CAS server appends the authorized ticket and redirects back to the service URL. This is what fiddler shows.

     http://localhost:25451/account/login?returnUrl=/shared/download?documentGroup=133&applicationId=3153&ticket={redacted}
    
  4. Here is the issue. The returnuRL in the login method is simply

    /shared/download?documentGroup=133.  
    

    The returnUrl no longer has the applicationId.

Interestingly enough, the line works just fine.

var ticket = Request.QueryString.Get("ticket");

I have tried to encode the whole serviceUrl and tried to encode just the returnUrl(see below) but I get the same missing ApplicationId issue.

[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
    var ticket = Request.QueryString.Get("ticket");

    if (!string.IsNullOrEmpty(ticket))
    {
        //verify the ticket...
        return RedirectToLocal(returnUrl);
    }

    var serviceUrl = Request.Url.Scheme + System.Uri.SchemeDelimiter + Request.Url.Host + (Request.Url.IsDefaultPort ? "" : ":" + Request.Url.Port) + "/account/login" + "?returnUrl=" + HttpUtility.UrlEncode(returnUrl);
    var authenCasUrl = string.Format("{0}login?service={1}", "https://{redacted}/", serviceUrl);
    return Redirect(authenCasUrl);
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
salli
  • 722
  • 5
  • 10
  • 1
    if you invert the two parameters documentGroup=133&applicationId=3153 => applicationId=3153&documentGroup=133 is always the first parameter persisted ? – B. Lec Jun 25 '21 at 07:28
  • Thanks for the response @B.Lec. Switching the variables dropped the documentId. So it seems that 2nd parameter is dropped regardless of the order. – salli Jun 28 '21 at 18:29
  • I have no clue on what is appenning, as a workaround you should try to use only 1 "state" parameter and serialize all you need in its value as a base64 encoded json – B. Lec Jun 29 '21 at 06:30
  • have a look at https://stackoverflow.com/questions/372865/path-combine-for-urls Flurl supports a url.combine that handels all the url combining magic. – stefan.seeland Jun 29 '21 at 11:00

2 Answers2

1

I have no experience with asp or SSO, but you may need to also HttpUtility.UrlEncode the value of the serviceUrl variable?

var authenCasUrl = string.Format("{0}login?service={1}", "https://{redacted}/", HttpUtility.UrlEncode(serviceUrl));

Since the service parameter is decoded by the CAS once, and then the value of returnUrl gets decoded by your server.

var returnUrl = "/shared/download?documentGroup=133&applicationId=3153";
var serviceUrl = "http://localhost:25451/account/login?returnUrl=" + HttpUtility.UrlEncode(returnUrl);
var casUrl = "https://{redacted}/cas/login?service=" + HttpUtility.UrlEncode(serviceUrl);

Which gives:

serviceUrl = http://localhost:25451/account/login?returnUrl=%2Fshared%2Fdownload%3FdocumentGroup%3D133%26applicationId%3D3153
casUrl     = https://{redacted}/cas/login?service=http%3A%2F%2Flocalhost%3A25451%2Faccount%2Flogin%3FreturnUrl%3D%252Fshared%252Fdownload%253FdocumentGroup%253D133%2526applicationId%253D3153

Explanation attempt:
  • You make a HTTP request to the CAS server. It's implementation splits the query parameters and decodes each value (and possibly key). One of which is the service parameter and is now (after decoding) a valid URL.
  • The CAS server makes a HTTP request with the URL from the service parameter (to your server) with the ticket appended.
  • You split the query parameters and decode each value (and possibly key).

If you only encoded the returnUrl once, your serviceUrl will look like what you showed in your third point:

http://localhost:25451/account/login?returnUrl=/shared/download?documentGroup=133&applicationId=3153&ticket={redacted}

How does the algorithm splitting the query string differentiate between a ? or & in the serviceUrl and the ones in the returnUrl? How should it know that ticket does not belong to the returnUrl?

As you can see in my code above, you are not encoding the returnUrl twice.
You are putting one URL in the parameters of another URL and then you put that URL in the parameters of a third URL.

You need to call UrlEncode for each value (and possibly key) when you put together a query. It does not matter whether that value is a URL, JSON, or arbitrary user input.

dmtweigt
  • 83
  • 4
  • This works and I will award the bounty. Before I accept it as the answer, I would like to know why does it work? Does not make sense to double encode the returnUrl. – salli Jun 29 '21 at 17:05
  • can you change your code from HttpUtility.Encode to HttpUtility.UrlEncode? Thank you! – salli Jun 29 '21 at 17:13
  • @salli I've added an explanation as requested and changed `Encode` to `UrlEncode`. I hope this is understandable. – dmtweigt Jul 01 '21 at 08:21
0

Since this site will be actually called by your URL, I don't think they just throw away parts of it. Lets try something here since I have encountered a similar problem with parameter in url strings in combination with asp.NET.

First, lets get the unedited URL from your Request:

string UneditedUrl = Request.RawUrl;

Since we are not needing anything before the ? mark, we shorten it a little bit:

string QueryString = (UneditedUrl.IndexOf('?') < UneditedUrl.Length - 1) ? UneditedUrl.Substring(UneditedUrl.IndexOf('?') + 1) : String.Empty;

This line also includes the possibility on neither having a ? mark or parameters and will return an empty string if so. Just for good measure, we don't want any exceptions here. Here you can check QueryString if it has both or more of your parameters you entered.

If there are not complete here, its not your code at fault. Something will already work on your URL before you do, probably your host then. Maybe check the settings of your IIS.


If your parameters are correctly in the edited QueryString, you can continue getting them by following this:

I learned that there is a way to let your framework do the job of parsing parameters into name/value collections. So lets give it a go:

NameValueCollection ParaCollection = HttpUtility.ParseQueryString(QueryString);

You can now check you params and their values by either using an index like ParaCollection[0] or ParaCollection["documentGroup"].

EDIT: I've found the question which brought me to the conclusion of using Request.RawUrl. Since this may not be the answer, it will maybe help a little bit more to understand that Request.RawUrl is the actual URL the user called and not the one the server executes: RawURL vs URL

Cataklysim
  • 637
  • 6
  • 21