Signup with username & social connections

I´m using the auth0-lock version 10.6.0 with angular2. I am currently allowing sign up with three connections: database, facebook & twitter.
I whould like all users to have a unique username that sign up to my app. I´ts really easy with database and works great but is it possible to force users from social connections to create their own username when signing up?

1 Like

Yes definitely, this would involve a two-step authentication flow, the base way would be to use Redirect Rules to handle this. I am documenting the steps for that below, Please note that these steps apply to a multi-step signup where you want to add any extra information to app_metadata etc to the user.

  • If you want to user to be able to login with the username and password,

    1. User signs up with social connection, after checking email and email_verified. You can check if the user has a connected identity with a database user in user.identities array. If such an identity exists, the rule will pass through if not, the rule will redirect the user to a special page which asks the user for their username and a password.

    2. On this page, you can setup your API server so that it accepts, a username-password pair from this page and creates a secondary identity for that user using Management API v2. The page will then redirect the user back to the /continue endpoint and when #1> occurs it lets the user pass through.

  • If you simply want the user to have a username created for the profile

  1. Step 1 will remain the same, However, the page will simply ask for a username.
  2. Step 2 will simply add the username to app_metadata.username, in this case, you’ll need to fetch if any users have the same username in order to maintain the uniqueness with Management API v2 Search User and then add app_metadata via the update user command.

The following is the example case 1, where you would want the user to be able to optionally signup with a username and password, However, the key concepts will stay the same with app_metadata.

function (user, context, callback) {

    if (context.protocol === 'redirect-callback') {
       if (context.request.query.error) {
          return callback(new UnauthorizedError("There was a problem signing you up"));
       }
    }
   
    function hasDatabaseIdentity (identities) {
        return identities.filter(function (identity) {
             identity.connection === 'DATABASE_CONNECTION_ID'
        }).length > 0;
    }

    if (hasDatabaseIdentity(user.identities) === false) {
       const jwt = require('jsonwebtoken');

       const token = jwt.sign({
            user_id: user.user_id, 
            email: user.email,
            email_verified: user.email_verified
       }, configuration.sharedToken, {
           audience: 'https://your_server_route.com/create-identity',
           issuer: 'auth0/rule'
       });

       context.redirect = {
         url: `https://your_server_route.com/create-identity?token=${token}`
       }
    }
    
    return callback(null, user, context);
}

On your client side code you can use a client side framework to build a form like this,

<form method="POST" action="https://your_server_route.com/create-identity?token=${token}&state=${state}">
     <input placeholder="username" />
     <input type="password" placeholder="password" />
     <input type="submit" value="Button"/>
</form>

On the server side, It should be some thing like this (I’m using NodeJS for the example)

app.get('create-identity', async function (req, res) {
    res.status(200).end(`
      <form method="POST" action="https://your_server_route.com/create-identity?token=${req.query.token}&state=${req.query.state}">
          <input name="username" id="username" placeholder="username" />
          <input name="password" id="password" type="password" placeholder="password" />
          <input type="submit" value="Button"/>
      </form>
    `);
});

// You should be using the express-jwt middleware.
const jwtMiddleware = expressJwt({ 
  secret: configuration.sharedToken,
  audience: 'https://your_server_route.com/create-identity',
  issuer: 'auth0/rule',
  getToken (req) {
      return req.query.token || null;
  }
});


// auth0 variable is an Auth0 management api instance from `node-auth0` package 
// https://github.com/auth0/node-auth0/
app.post('create-identity', jwtMiddleware, async function (req, res) {
   const {username, password} = req.body;
   const {user_id, email, email_verified} = req.user;
   try{
      const createdUser = await auth0.users.create({
          connection: 'DATABASE_CONNECTION_ID',
          email_verified,
          username,
          password,
          email,
      });

      await auth0.users.link(user_id, {
         connection: 'DATABASE_CONNECTION_ID',
         user_id: createdUser.user_id,
         provider: 'auth0'
      });

      res.redirect('https://your-domain.auth0.com/continue' + '?state=' + req.query.state);
   }catch(e){
      res.redirect('https://your-domain.auth0.com/continue?error=CANT_CREATE&state=' + req.query.state);
   }
});

2 Likes

Thanks.
I was hoping for a easier solution :stuck_out_tongue:
But i´m using ASP.NET Core (net461) for the backend. Can you provide an example for that?

Best regards,
Kjartan

One other thing? The redirect, will it be inside the auth0 popup and share the same style?

Best regards,
Kjartan

After re-reading your question, I updated my answer (I was wondering if all you want is a unique username or you want the user to be able to login using the username/password account my first answer only included the case when you want the user to be able to login, however I have updated code)

No, the endpoint handling this will be your own so you can customize this with your applications style etc.

This would be the code in .Net for the first scenario

public class AccountController : Controller
{
    public async Task<ActionResult> CreateIdentity(string token, string username, string password, string state)
    {
        var yourApiToken = "yourApiToken";
        var yourBaseUri = "yourBaseUri";

        var secret = Encoding.UTF8.GetBytes("your secret");
            // or, if using base64 encoded secret:
            // var secret = Encoding.UTF8.GetBytes(Base64UrlEncoder.Decode("your_base64_encoded_secret"));

            // requires NuGet package 'System.IdentityModel.Tokens.Jwt'
            var tokenValidationParameters = new TokenValidationParameters
                                                {
                                                    ValidAudience = "https://your_server_route.com/create-identity",
                                                    ValidIssuer = "auth0/rule",
                                                    IssuerSigningKey = new SymmetricSecurityKey(secret)
                                                };
            var tokenHandler = new JwtSecurityTokenHandler();
            SecurityToken validatedToken;
            var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out validatedToken);
            var identity = (ClaimsIdentity)principal.Identity;

            var userId = identity.FindFirst("user_id").Value;
            var email = identity.FindFirst("email").Value;
            var emailVerified = identity.FindFirst("email_verified").Value == "true";

            var client = new Auth0.ManagementApi.ManagementApiClient(yourApiToken, yourBaseUri);

            try
            {
                // first create the user
                var createdUser = await client.Users.CreateAsync(
                    new UserCreateRequest()
                        {
                            Connection = "DATABASE_CONNECTION_ID}",
                            UserName = username,
                            Password = password,
                            Email = email,
                            EmailVerified = emailVerified
                        });

                await client.Users.LinkAccountAsync(
                    userId,
                    new UserAccountLinkRequest()
                        {
                            ConnectionId = "DATABASE_CONNECTION_ID}",
                            Provider = "auth0",
                            UserId = createdUser.UserId
                        });
            }
            catch (Exception)
            {
                return this.Redirect("https://your-domain.auth0.com/continue?error=CANT_CREATE&state=" + state);

            }
            return this.Redirect("https://your-domain.auth0.com/continue?state=" + state);

        }
    }

Notice that I’m using an CreateIdentity action in an Account controller, so in the audience and redirect url in the rule the endpoint would be something like https://yourserver/Account/CreateIdentity.

1 Like

Thanks. But what about the get method?

Yes I only want unique username :slight_smile:

I was hoping this would be integrated into lock widget because this is possible with database connection but not with social :frowning:

It is not integrated as the exact specifics would different depending on the Identity Provider and the business logic of the application itself. The twostep signup as I discussed in the second approach is the best way to do that (:

This would be a nice feature to the lock widged, do you agree @nicolas_sabena and @Abhishek_Hingnikar ? Are you not planning to implement this? Seems like a very common use case. And also this is already possible with database connections so…

We do not have any plans to implement it out of the box, mostly because of the flow of how it would work with multiple Identity Providers and linked accounts. For example, you can setup a rule to fetch this information straight from twitter if twitter is the only social IdP, or you might optionally do it in case of a conflict etc. Because of this we leave this onto you, on the exact details of how you will handle this process. However, I have forwarded your request to the crew responsible.

Yes. But I cant see how anything is different. It would just require a extra step to choose a unique username.

@Abhishek_Hingnikar @nicolas_sabena

Would it not be a great feature if the lock widget would add an optional second step for social signup for adding extra properties like username?

Auth0 is suppose to save us devs from this hassle. Now I need to do a custom signup form and the benefits of auth0 is fading away…

And I opened a ticket for auth0 support 10 days ago and no answer :-1:

@Abhishek_Hingnikar

I had to make minor changes to save this rule (see below) but i get the following error:

{
  "code": 500,
  "error": "Script generated an unhandled asynchronous exception.",
  "details": "TypeError: secret must be a string or buffer",
  "name": "TypeError",
  "message": "secret must be a string or buffer",
  "stack": "TypeError: secret must be a string or buffer\n    at typeError (/data/_verquire/auth0-horn-middlewares/0.0.4/node_modules/jwa/index.js:16:10)\n    at Object.sign (/data/_verquire/auth0-horn-middlewares/0.0.4/node_modules/jwa/index.js:32:13)\n    at Object.jwsSign [as sign] (/data/_verquire/auth0-horn-middlewares/0.0.4/node_modules/jws/lib/sign-stream.js:23:24)\n    at Object.JWT.sign (/data/_verquire/auth0-oauth2-express/0.0.3/node_modules/jsonwebtoken/index.js:137:16)\n    at async.waterfall.callback.user (/data/webtask.js:54:24)\n    at fn (/data/sandbox/node_modules/auth0-authz-rules-api/node_modules/async/lib/async.js:638:34)\n    at Immediate._onImmediate (/data/sandbox/node_modules/auth0-authz-rules-api/node_modules/async/lib/async.js:554:34)\n    at processImmediate [as _immediateCallback] (timers.js:383:17)"
}

Rule:

function (user, context, callback) {

if (context.protocol === 'redirect-callback') {
   if (context.request.query.error) {
      return callback(new UnauthorizedError("There was a problem signing you up"));
   }
}

function hasDatabaseIdentity (identities) {
    return identities.filter(function (identity) {
         return identity.connection === 'xxxxx';
    }).length > 0;
}

if (hasDatabaseIdentity(user.identities) === false) {
   var jwt = require('jsonwebtoken');

   var token = jwt.sign({
        user_id: user.user_id, 
        email: user.email,
        email_verified: user.email_verified
   }, configuration.sharedToken, {
     audience: 'http://localhost:3000/signup',
       issuer: 'auth0/rule'
   });

   context.redirect = {
     url: 'http://localhost:3000/signup?token=' + token
   };
}

return callback(null, user, context);

}

Hi, the error is being caused as you have not setup the configuration. The code assumes that you set sharedToken in both your API and Rules. To add sharedToken to Rules, please goto https://manage.auth0.com/#/rules then scroll down and add it in settings.

1 Like

Thanks @Abhishek_Hingnikar

But there is one more problem. I am developing a SPA application and I would prefer to have lock option auth.redirect = false because I´m using routing without hash(#) in my app. Otherwise the redirect would fail because I cannot cover all url´s with wildcard in “Allowed callback Urls”. Maybe i´m just misunderstanding this… But auth.redirect = false solved that problem but now with the new rule, the redirect takes place in new browser window :frowning:

Can I use auth.redirect = false with some kind of a wildcard url in allowed callback Urls" ( e.g. mydomain.com/* ) ?? I prefer not not use hash routing because we need to be extra SEO friendly.

If this is not possible, what is the recommended way?

When auth.redirect is false redirection always takes place in a new browser window. This will however, fail in many cases such as non-standard browsers etc. We strongly recommend using redirect mode over popup mode.

Regarding the redirect mode, the most transparent way of handling it would be to have a Single Url for callback and configuring lock to redirect to this URL after the login process has completed and then resuming the state (this is what most apps today do and is considered a best practise). There are many solutions on how to achieve this

  • You can either store the state of the SPA into localStorage and reference it using the state parameter, upon login the state parameter is returned as is, you can use this to extract the original state and resume the spa to its original state. The entire process would look something like
/* before lock.show */
const state = uuid.v4(); // assuming node-uuid
localStorage[state] = JSON.stringify(appState); // This can include url or anything you'll need to resume your app

lock.show({
  auth: {
    params: {
       state: state
    }
  }
});


/* Now to handle this */

lock.on('authenticated', function (authResult){
   const originalState = JSON.parse(localStorage[authResult.state] || 'null');
   if(originalState){
      /* say originalState.url represents where authentication began */
      router.redirect(originalState.url); // <-P lease change this to match the API of the router you are using
   }
});
  • Alternatively you can pass the redirectUrl with a queryString (for example ?fwd=/some/url) and redirect to this url after fetching the callback.

I strongly recommend the first solution, as it restricts possibility of attacks/hijacking etc.

1 Like

@Abhishek_Hingnikar
Thank you!!! :thumbsup:

1 Like

@Abhishek_Hingnikar @nicolas_sabena

I don´t understand the ASP.Net version. The user is not logged in at this stage so I cannot have the Authorized attribute there? Should I send the token as a query parameter and validate that somehow with the Auth0 Management SDK?
Do you have a example for me?
Or am I maby on the wrong path. I tried using angular-jwt to post and the Authorize attribute but always got unothorized…

Sorry, you’re right. I just corrected the code.