Is it possible to register a user programmatically?

Hi,

I am working on a project where we put an API Gateway in front of a Speckle Server and some other things.

I am looking for a way to register users on my Speckle Server from the backend layer, and not from the Speckle frontend. The main reason is because I want to relate Speckle Accounts to my general User Account.

Is that possible?

1 Like

I’ve wrestled with this a little. You may (with appropriate auth scope) make a server invite or stream invite. This is no guarantee of user creation and doesn’t yield a proto-id, say.

There a few spots dotted around the server code that were you running your own, you could join the dots by listening to user_creation that matches those invites.

Consider it “eventual consistency” powered by meat.

packages/server/modules/serverinvites/graph/schemas/serverInvites.graphql

I haven’t tried but it may be possible to add a webhook programmatically for the user_create event. (I don’t see any validation on the event types). But, it’s excluded from the web UI so @dimitrie may disapprove of the suggestion. :rofl:

3 Likes

Interesting. Thanks for pointing it out @jsdbroughton :blush:. But it doesn’t really solve my issue.

I am trying to avoid the user having to create multiple accounts: 1 for my application, 1 for Speckle.

Ideally I would create that account for him, and tag it’s Speckle_Account_Id to my general User model. But I am very open to suggestions.

1 Like

:thinking: Are you looking into hiding the Speckle account completely to the user?

Not an OAuth expert here… But usually, the way other apps i’ve seen pulling this off was to allow to link your account, which basically just sends the user through the auth page of the service to get the access_token and refresh_token. You can then store these (hopefully encrypted) in your DB linked to your user. Every time your user needs Speckle from that point onward, you can use the tokens in your DB (either on the backend or the frontend)

Since the /auth methods return a redirect response. You may need to pull this off in the frontend as part of your registration process:

  • User fills in form in frontend and hits ok
  • You register your user in the backend
  • Frontend receives successful registration
  • You then start the speckle registration using the user details by calling /auth/local/register with email and pwd
  • Speckle will return a redirect with the `access_token``
  • Swap that for the real tokens, and store them in your DB

It may be that the web team screams at me after suggesting this though…:sweat_smile:

2 Likes

Haha @AlanRynne, no screaming necessary :smiley:

That’s tricky unfortunately. I remember @Baris & the team at Ekkodale did something similar, but unsure how they achieved it. If you have direct control over the database - as it seems you do - it might be easiest to insert a record in the speckle db directly from your app for the respective user.

The currently supported way is to register your application on the speckle server as a third party app (or first party, if you control the deployment) and thereafter request the user’s permission for your app do to various things on their behalf (as defined through the scopes that you specifiy when you register your app). Once you have that, you can definitively tie in their account with your internal account!

4 Likes

That will do the trick. Thank you all! :muscle:

2 Likes

Hi! Sorry for my late answer. Maybe it helps to people in future. Here is the code snippet on .NET to register user.

        /// <summary>
        /// Registers a new user or logs with the existing user in.
        /// </summary>
        /// <param name="serverUrl">Url of Webserver</param>
        /// <returns>Access and refresh Tokens.</returns>
        public async Task<JObject> LoginToSpeckle(string serverUrl)
        {
            string challenge = Guid.NewGuid().ToString();
            string registerUrl = $"{serverUrl}/auth/local/register?challenge={challenge}";
            string loginUrl = $"{serverUrl}/auth/local/login?challenge={challenge}";

            HttpClientHandler httpClientHandler = new HttpClientHandler
            {
                AllowAutoRedirect = false 
            };

            using (HttpClient _httpClient = new HttpClient(httpClientHandler))
            {
                object userCredentials = new
                {
                    password = Configuration["ADMINPASSWORD"],
                    name = Configuration["ADMINUSERNAME"],
                    email = Configuration["ADMINEMAIL"]
                };
                string json = JsonConvert.SerializeObject(userCredentials);

                StringContent data = new StringContent(json, Encoding.UTF8, "application/json");

                string access_code = await RegisterUser(data, _httpClient, registerUrl);

                if (access_code is null)
                {

                    access_code = await LoginUser(data, _httpClient, loginUrl);
                    if (access_code is null)
                    {
                        return null;
                    }
                }
                JObject tokens = await ExchangeAccessToken(serverUrl, access_code, _httpClient, challenge);
                _httpClient.Dispose();
                return tokens;
            }
        }

        /// <summary>
        /// Exchange accesCode for tokens.
        /// </summary>
        /// <param name="serverUrl"></param>
        /// <param name="access_code">Code needed for Token</param>
        /// <param name="_httpClient">Client to send request and receive responses</param>
        /// <param name="generatedChallenge">Random challenge</param>
        /// <returns>Access and Refresh Token</returns>
        private async Task<JObject> ExchangeAccessToken(string serverUrl, string access_code, HttpClient _httpClient, string generatedChallenge)
        {
            var requestBody = new
            {
                accessCode = access_code,
               // You have to use this because in database generated access codes with another 3rd Party 
               // applications that you can add on speckle interface 
               // will be always replaced somehow with the 
               // following value: spklwebapp
                appId = Configuration["APPID"], // == spklwebapp
                appSecret = Configuration["APPSECRET"], // == spklwebapp
                challenge = generatedChallenge
            };
            string jsonRequestBody = JsonConvert.SerializeObject(requestBody);

            StringContent accessTokenData = new StringContent(jsonRequestBody, Encoding.UTF8, "application/json");

            string accessTokenUrl = $"{serverUrl}/auth/token";

            var response = await _httpClient.PostAsync(accessTokenUrl, accessTokenData);

            if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
            {
                return null;
            }

            var jsonString = await response.Content.ReadAsStringAsync();

            return JsonConvert.DeserializeObject(jsonString) as JObject;
        }

        /// <summary>
        /// Tries to login the user for an accessCode.
        /// </summary>
        /// <param name="data">User Data</param>
        /// <param name="httpClient">Client to send request and receive responses</param>
        /// <param name="loginUrl">Speckle endpoint for login</param>
        /// <returns>Access Code</returns>
        private async Task<string> LoginUser(StringContent data, HttpClient httpClient, string loginUrl)
        {
            var response = await httpClient.PostAsync(loginUrl, data);

            if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
            {
                return null;
            }

            string query = response?.Headers?.Location?.Query;

            if (string.IsNullOrEmpty(query))
            {
                return null;
            }

            return query.Split("access_code=")[1];
        }

        /// <summary>
        /// Tries to register a new user and recieve an accessCode.
        /// </summary>
        /// <param name="data">User data</param>
        /// <param name="httpClient">Client to send request and receive responses</param>
        /// <param name="registerUrl">Speckle endpoint for registering</param>
        /// <returns>Access Code</returns>
        private async Task<string> RegisterUser(StringContent data, HttpClient httpClient, string registerUrl)
        {

                var response = await httpClient.PostAsync(registerUrl, data);

                if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
                {
                    Console.WriteLine("User exists already. We will login.");
                    return null;
                }

                string query = response?.Headers?.Location?.Query;

                if (string.IsNullOrEmpty(query))
                {
                    return null;
                }

                return query.Split("access_code=")[1];
            }
      

        }
4 Likes