Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Forgot password URL

Tags:

c#

url

asp.net

I have a web application which uses an asp.net login control. In addition, I also uses a password recovery control for users to recover their password. Once user had finished entering their particulars in the recovery control, an email containing a verification URL would be send to the user's email address. Upon clicking the URL, it would direct the user into the UserProfile of my web application, which inside, it allows the user to change their password.

Now the problem is, because i set an access rule to UserProfile.aspx to deny anonymous user, when I redirect from the URL into the UserProfile.aspx page, it direct me to the LoginPage instead(the system recognize the me as an anonymous user).

Why is it so? Is there anywhere that I could direct into the userprofile page once the URL is clicked(which include all user information) ?

The URL look like this:

http://localhost:1039/Members/UserProfile.aspx?ID=56f74cc7-7680-4f1b-9207-0ab8dad63cad 

Where the last part of the URL was actually the userId.

Here is the code for userprofile aspx:

<asp:SqlDataSource ID="SqlDataSource1" runat="server" 
              ConnectionString="<%$ ConnectionStrings:ASPNETDBConnectionString1 %>" 
              SelectCommand="SELECT aspnet_Membership.Email, Details.CustName, Details.CustNum, Details.CustRole, Details.CustStatus, Details.PName, Details.PEmail, Details.PRole, Details.WedDate, aspnet_Users.UserName, Details.UserId FROM Details INNER JOIN aspnet_Membership ON Details.UserId = aspnet_Membership.UserId INNER JOIN aspnet_Users ON aspnet_Membership.UserId = aspnet_Users.UserId WHERE (Details.UserId = @UserId)" 


              UpdateCommand="update Details SET CustName = @CustName, CustNum = @CustNum, CustRole = @CustRole, CustStatus = @CustStatus, PName = @PName, PEmail = @PEmail, PRole = @PRole, WedDate = @WedDate WHERE [UserId] = @UserId

                            Update aspnet_Membership Set Email= @email WHERE [UserId] = @UserId"

              DeleteCommand= "DELETE FROM Details WHERE UserId = @UserId;"> 

              <DeleteParameters>
                  <asp:ControlParameter ControlID="lblHidden" Name="UserId" PropertyName="Text" 
                      Type="String" />
              </DeleteParameters>

              <SelectParameters>
                  <asp:ControlParameter ControlID="lblHidden" Name="UserId" PropertyName="Text" />

              </SelectParameters>

              <UpdateParameters>
                  <asp:Parameter Name="CustName" />
                  <asp:Parameter Name="CustNum" />
                  <asp:Parameter Name="CustRole" />
                  <asp:Parameter Name="CustStatus" />
                  <asp:Parameter Name="PName" />
                  <asp:Parameter Name="PEmail" />
                  <asp:Parameter Name="PRole" />
                  <asp:Parameter Name="WedDate" />
                  <asp:Parameter Name="UserId" />
                  <asp:Parameter Name="email" />
              </UpdateParameters>


          </asp:SqlDataSource>
          <asp:DetailsView ID="DetailsView1" runat="server" AutoGenerateRows="False" 
              DataSourceID="SqlDataSource1" Height="50px" Width="125px">
              <Fields>
                  <asp:BoundField DataField="Email" HeaderText="Email" SortExpression="Email" />
                  <asp:BoundField DataField="CustName" HeaderText="CustName" 
                      SortExpression="CustName" />
                  <asp:BoundField DataField="CustNum" HeaderText="CustNum" 
                      SortExpression="CustNum" />
                  <asp:BoundField DataField="CustRole" HeaderText="CustRole" 
                      SortExpression="CustRole" />
                  <asp:BoundField DataField="CustStatus" HeaderText="CustStatus" 
                      SortExpression="CustStatus" />
                  <asp:BoundField DataField="PName" HeaderText="PName" SortExpression="PName" />
                  <asp:BoundField DataField="PEmail" HeaderText="PEmail" 
                      SortExpression="PEmail" />
                  <asp:BoundField DataField="PRole" HeaderText="PRole" SortExpression="PRole" />
                  <asp:BoundField DataField="WedDate" HeaderText="WedDate" 
                      SortExpression="WedDate" />
                  <asp:BoundField DataField="UserName" HeaderText="UserName" 
                      SortExpression="UserName" />
                  <asp:BoundField DataField="UserId" HeaderText="UserId" 
                      SortExpression="UserId" />
                  <asp:CommandField ShowEditButton="True" />
              </Fields>
          </asp:DetailsView>
          <asp:Label ID="lblHidden" runat="server" Text="Label" Visible="False"></asp:Label>



          <asp:Button ID="btnDelete" runat="server" onclick="btnDelete_Click" 
              Text="Delete" />

Here is the code behind:

protected void Page_Load(object sender, EventArgs e)
    {
         MembershipUser currentUser = Membership.GetUser();
        lblHidden.Text = currentUser.ProviderUserKey.ToString();
    }

    protected void SqlDataSource1_Selecting(object sender, SqlDataSourceSelectingEventArgs e)
    {
        // Get a reference to the currently logged on user
        MembershipUser currentUser = Membership.GetUser();

        // Determine the currently logged on user's UserId value
        // Assign the currently logged on user's UserId to the @UserId parameter
        //access the parameter value using e.Command.Parameters 
        //programmatically set the @UserId:
        e.Command.Parameters["@UserId"].Value = currentUser.ProviderUserKey.ToString();



    }
    protected void btnDelete_Click(object sender, EventArgs e)
    {

        SqlConnection connection = new SqlConnection();
        connection.ConnectionString = ConfigurationManager.ConnectionStrings["ASPNETDBConnectionString1"].ConnectionString;
        SqlCommand cmd = new SqlCommand();
        SqlCommand cmd1 = new SqlCommand(); 
        string userId = lblHidden.Text;

        cmd.Connection = connection;
        cmd.CommandText = "DELETE FROM Details WHERE UserId ='" + userId + "'";


        cmd1.Connection = connection;
        cmd1.CommandText = "DELETE FROM aspnet_Membership WHERE UserId ='" + userId + "'"; 

        connection.Open();

        cmd.ExecuteNonQuery();
        cmd1.ExecuteNonQuery();


        connection.Close();


      Response.Redirect("Home.aspx");
    }

Secondly, is there any way I could set an expiry to the URL? If the URL is being click the second time, it would not redirect the user to any place. I saw many posts, most of them recommend to add a column into the database. Is there any other way where I could set the expiry without touching the database???

like image 342
user1529419 Avatar asked Jul 16 '12 16:07

user1529419


2 Answers

Consider a separate page for the changepassword link. Have this page take a unique identifier. This identifier should work one time only, have an expiry date, and be specific to that user. Make this page public:

<location path="changepassword.aspx">
 <system.web>
   <authorization>
     <allow users="*"/>
   </authorization>
 </system.web>
</location>

You need to store the unique identifier somewhere against the user. If you don't want to affect your current schema, you can create a new table:

PK | Identifier | UserID                               | expires
1  | abcd       | ffffffff-ffff-ffff-ffff-ffffffffffff | 16-jul-2012 18:26

When the page is requested, if the identifier is expired don't allow the page to function. Once the password has been changed, invalidate the identifier - either delete it, or set the expiry date to something in the past (e.g. now).

like image 52
Paul Fleming Avatar answered Oct 10 '22 16:10

Paul Fleming


This isn't a direct response to the question asked but a more general comment on structuring password reset tools...

When coding this functionality I'd do a couple of things differently.

Access to the profile page or just change password?

Firstly if the user just needs to change their password they don't need access to the user profile page just the change password page. I would take them straight to a "change password" page and make this specific to the pasword reset functionality and thus have it anonymously available with no problems.

Why not authenticate them and then send them to the profile page?

The other alternative is rather than mess around with anonymous access to things just do an auto-login. That url would lead to a landing page that would log the given user in automatically (you have all the user details after all). Then they can be redirected to the profile page and they are no longer anonymous so its all good.

The problems with plain text user ids

You need to use a better token. If somebody can find the user id of another user (perhaps it shows up in the url if you click to see a users public profile) then they can change that persons password. That is not cool as I'm sure you know.

What do I use instead of plaintext user ids?

Random Tokens

The two methods around this are creating random tokens that you store in the database and then verify them when loading the page. This then of course makes it very easy to invalidate them by just changing something in the database when you want to invalidate them. You can just store a random token (I'd probably go for a base64 encoded string that is about 16 characters long - effectively 96 bits of randomness) in the DB along with any necessary information (eg userid, creation date (or expiry date), etc.). If you want it to be one use you can just clear the token out of the DB once its been validated once (or mark a field to say its been used or any number of other alternatives).

Encrypted Tokens

The more secure method that doesn't involve touching the database is to create an encrypted token that you can pass to the user. This token could contain the userID and the time the token was created (and any other information you feel like) and would be put in the e-mail and forgotten about. Because it is encrypted data rather than a random token on postback rather than validating via the database you can just decrypt the given token to fidn the user and how old the token is.

If you want to make them one use without hitting the database its a bit more tricky but I'd probably go for storing the generated tokens in the server application dictionary and if the token isn't in there it isn't valid and you remove it from the list you are storing once its used. You also clear up old tokens routinely to ensure that you aren't storing too much junk. This approach also has the drawback that if the app is restarted you would clear the list of tokens and thus invalidate them all. This is only a problem if you need them to be one shot though. Personally I'd let them reset their password all they want in whatever timeframe you determine appropriate with a single token. :)

Another warning about plaintext user IDs in the original scenario

I can't stress enough how careful you need to be with just putting the userid in plaintext in the url as the only information passed. The moment somebody finds out another person's userID their account is effectively compromised. They just have to put that persons userid into the url and they can change their password.

Even if you don't have anywhere that exposes other people's UserIDs now you have to be 100% sure that you never will in the future either which in practice is impossible to guarantee.

like image 39
Chris Avatar answered Oct 10 '22 18:10

Chris