Skip to content

Instantly share code, notes, and snippets.

@koakh
Last active April 6, 2024 02:04
Show Gist options
  • Star 40 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save koakh/fbbc37cde630bedcf57acfd4d6a6956b to your computer and use it in GitHub Desktop.
Save koakh/fbbc37cde630bedcf57acfd4d6a6956b to your computer and use it in GitHub Desktop.
SurrealDB : How to use signin and signup (server + client nodesjs)
**As a developer you have complete control over the signup and signin functionality...**
```shell
$ surreal sql --conn http://localhost:8000 --user root --pass root --ns test --db test
```
```sql
-- optional : this is the default see --ns test --db test start server flags
-- USE NS test DB test;
-- define TABLE user SCHEMAFULL PERMISSIONS FOR select, update WHERE id = $auth.id FOR create, delete NONE; // miss ,
-- Tobie: However, make it schemaless, as you haven't defined fields.
-- SCHEMAFULL if we defined fields else use SCHEMALESS
---opt1:SCHEMALESS
-- DEFINE TABLE user SCHEMALESS
-- PERMISSIONS
-- FOR select, update WHERE id = $auth.id,
-- FOR create, delete NONE;
-- warn with opt1 if we re signup again it will create another user with same email, and after it all signin's will fail,
-- to fix this issue always use next opt2 that have a UNIQUE INDEX, and prevents create a user with same email, and this is the expected behavior
---opt2:SCHEMAFULL
DEFINE TABLE user SCHEMAFULL
PERMISSIONS
FOR select, update WHERE id = $auth.id,
FOR create, delete NONE;
DEFINE FIELD user ON user TYPE string;
DEFINE FIELD pass ON user TYPE string;
DEFINE FIELD settings.* ON user TYPE object;
DEFINE FIELD settings.marketing ON user TYPE string;
DEFINE FIELD tags ON user TYPE array;
DEFINE INDEX idx_user ON user COLUMNS user UNIQUE;
-- define scope
DEFINE SCOPE allusers
-- the JWT session will be valid for 14 days
SESSION 14d
-- The optional SIGNUP clause will be run when calling the signup method for this scope
-- It is designed to create or add a new record to the database.
-- If set, it needs to return a record or a record id
-- The variables can be passed in to the signin method
SIGNUP ( CREATE user SET settings.marketing = $marketing, user = $user, pass = crypto::argon2::generate($pass), tags = $tags )
-- The optional SIGNIN clause will be run when calling the signin method for this scope
-- It is designed to check if a record exists in the database.
-- If set, it needs to return a record or a record id
-- The variables can be passed in to the signin method
SIGNIN ( SELECT * FROM user WHERE user = $user AND crypto::argon2::compare(pass, $pass) )
-- this optional clause will be run when calling the signup method for this scope
```
ctrl+c
check info db in shell
```shell
$ echo "INFO FOR DB;" | surreal sql --conn http://localhost:8000 --user root --pass root --ns test --db test | jq
```
check info db in REPL
```shell
$ surreal sql --conn http://localhost:8000 --user root --pass root --ns test --db test --pretty
```
```sql
INFO FOR DB;
```
```json
[
{
"time": "249.753µs",
"status": "OK",
"result": {
"dl": {},
"dt": {},
"sc": {
"allusers": "DEFINE SCOPE allusers SESSION 2w SIGNUP (CREATE user SET settings.marketing = $marketing, email = $email, pass = crypto::argon2::generate($pass), tags = $tags) SIGNIN (SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass))"
},
"tb": {
"user": "DEFINE TABLE user SCHEMALESS PERMISSIONS FOR select WHERE id = $auth.id, FOR create NONE, FOR update WHERE id = $auth.id, FOR delete NONE"
}
}
}
]
```
Then you could use the **following query from the client to signup (create) a new user**, or **signin (select) an existing user**...
```js
db.signup({
NS: 'my_ns',
DB: 'my_db',
SC: 'allusers', // We want to signup to the 'allusers' scope defined above
email: 'test@acme.com', // We can add any variable here to use in the SIGNUP clause
pass: 'some password', // We can add any variable here to use in the SIGNUP clause
marketing: true, // We can add any variable here to use in the SIGNUP clause
tags: ['rust', 'golang', 'javascript'], // We can add any variable here to use in the SIGNUP clause
});
// you should receive a jwt token
db.signin({
NS: 'my_ns',
DB: 'my_db',
SC: 'allusers', // We want to signup to the 'allusers' scope defined above
email: 'test@acme.com', // We can add any variable here to use in the SIGNUP clause
pass: 'some password', // We can add any variable here to use in the SIGNUP clause
});
// you should receive a jwt token
```
@dustsucker
Copy link

I have a couple of questions. I run the setup, the SQL Part seems to work fine, but with JS I get an error: “There was a problem with authentication”.
First Question, do you have to be signed in as root for the signup to work? So:

db.signin({user:root,pass:root})
db.signup({
  NS: 'my_ns',
  DB: 'my_db',
  SC: 'allusers', // We want to signup to the 'allusers' scope defined above
  email: 'test@acme.com', // We can add any variable here to use in the SIGNUP clause
  pass: 'some password', // We can add any variable here to use in the SIGNUP clause
  marketing: true, // We can add any variable here to use in the SIGNUP clause
  tags: ['rust', 'golang', 'javascript'], // We can add any variable here to use in the SIGNUP clause
});

Because in my case this didn't work either
And with the namespaces, do you just need to do the SQL setup one time for all namespaces and db's ? Or is the SQL setup scoped to namespace or DB?
And is there some deeper Documentation available for SurrealDB on the Documentation Page I found nothing about the setup to get signup working.

@koakh
Copy link
Author

koakh commented Sep 14, 2022

hello @dustsucker

answear: no you don't need to be signin with root
(but I start the node server with root/root, see bellow notes)

you need to create the schemafull and start nodejs
nest create a user account with signup and use it after signin

answear: yes the sql part or is scoped to DB and NS,
see USE NS test DB test; in top of SQL

I test this signup and signin to test user,
next I change the NS and DB for ex to test2/test2 and signin fails, if change again the payload to test/test works againt

please follow bellow notes

# start server with defaults
$ surreal start --log trace --user root --pass root
# same as
$ surreal sql --conn http://localhost:8000 --user root --pass root --ns test --db test
-- create SCHEMAFULL

---opt2:SCHEMAFULL
DEFINE TABLE user SCHEMAFULL
....
get sql block from gist
...
ctrl+c

next start my node server in my case using nestjs with

SURREALDB_URL="http://127.0.0.1:8000/rpc"
SURREALDB_NAMESPACE="test"
SURREALDB_DATABASE="test"
SURREALDB_USER="root"
SURREALDB_PASS="root"
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      load: [configuration],
    }),
    SurrealDbModule.forRootAsync(SurrealDbModule, {
      useFactory: async (configService: ConfigService) => ({
        url: configService.get('SURREALDB_URL'),
        namespace: configService.get('SURREALDB_NAMESPACE'),
        database: configService.get('SURREALDB_DATABASE'),
        user: configService.get('SURREALDB_USER'),
        pass: configService.get('SURREALDB_PASS'),
      }),
   ...

next fire signup to create a new user in DB test NS test

$ curl --request POST \
  --url http://localhost:3000/surrealdb/signup \
  --header 'content-type: application/json' \
  --header 'user-agent: vscode-restclient' \
  --data '{"ns": "test","db": "test","sc": "allusers","email": "tobie@surrealdb.com","pass": "some password","marketing": true,"tags": ["rust", "golang", "javascript"]}'
{
  "accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE2NjMxODY1OTUsIm5iZiI6MTY2MzE4NjU5NSwiZXhwIjoxNjY0Mzk2MTk1LCJpc3MiOiJTdXJyZWFsREIiLCJucyI6InRlc3QiLCJkYiI6InRlc3QiLCJzYyI6ImFsbHVzZXJzIiwiaWQiOiJ1c2VyOjA4a3Fuc2Q4N3l0MTE5dnBvOTZ4In0.IvaNYIBpME1ZgJG4--mQYkiuldWFsSHQfuMVqCZhaaLgy4FH97W7jlpvnf2wABn13z7N1mg5qjiD-PFxmbuKlQ"
}

next I can signin in DB test and NS test with signup user

$ curl --request POST \
  --url http://localhost:3000/surrealdb/signin \
  --header 'content-type: application/json' \
  --header 'user-agent: vscode-restclient' \
  --data '{"ns": "test","db": "test","sc": "allusers","email": "tobie@surrealdb.com","pass": "some password"}'
{
  "accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE2NjMxODY4NjgsIm5iZiI6MTY2MzE4Njg2OCwiZXhwIjoxNjY0Mzk2NDY4LCJpc3MiOiJTdXJyZWFsREIiLCJucyI6InRlc3QiLCJkYiI6InRlc3QiLCJzYyI6ImFsbHVzZXJzIiwiaWQiOiJ1c2VyOjA4a3Fuc2Q4N3l0MTE5dnBvOTZ4In0.iM_srrAIYmBCz_bEVa9qZ3yLL5QqMy6T-1iINBH-jJ2HvH2l5S10TMzG9LWXtzksVo2qlo264BM26dQimO4xFA"
} 

and it works

if you change the payload from

{"ns": "test","db": "test","sc": "allusers","email": "tobie@surrealdb.com","pass": "some password"}

to

{"ns": "test"2,"db": "test2","sc": "allusers","email": "tobie@surrealdb.com","pass": "some password"}

you note that fails, because we change the DB and NS

I hope this helps

@dustsucker
Copy link

@koakh Thank you for your reply, It helped me a lot. Do you know a way of making the Databases dynamic, so I can assign an extra database to every user so for example user A should only have access to users/userA and userB should not have access to it. So, Basically a Private Database for every User? I could just execute the setup for every user that creates an account, but then I would have to use the root user in the application, and I don't want that.

Thank you for the explanation. : )

@koakh
Copy link
Author

koakh commented Sep 15, 2022

@dustsucker glad to know that I can help

Are you talk about something like multi tenant?

I think you can use databases and namespaces for this

When you singnin we start to be inside a scope, and don't have access to outer scope. Maybe you can try to don't expose signup endpoint and you create the users

ex user1@mail.com in db user1 and namespace user1
ex user2@mail.com in db user2 and namespace user2

And try to singin. I think user1 start to have its own database, with namespace,
And the same for user2

You can try create some data in both and check with selects if user1 see data from user2, and the other way, or better with INFO FOR DB in both signin's

But I never try it and now I'm on.phone

Just a raw idea, I can be wrong. This is a powerful db and new to, everybody is learning and have a lot fun with it, and help each other in the process

@georgyo
Copy link

georgyo commented Sep 16, 2022

This was super helpful!

Here is a slightly modified one that creates users as user:<username>

DEFINE SCOPE allusers 
SESSION 14d
SIGNUP (
  CREATE type::thing("user", string::lowercase(string::trim($id)))
  SET pass = crypto::argon2::generate($pass)
)
SIGNIN (
  SELECT * FROM type::thing("user", string::lowercase(string::trim($id)))
  WHERE crypto::argon2::compare(pass, $pass)
)

Because we are reusing the id, the username unique constraint is enforced even in a SCHEMALESS table.

However this also means that username will not be changeable in the future as all relationships will be using the old username as the key.

@bit-garden
Copy link

Can we have an example of how to use this token? I'm getting authorization errors when trying

curl -X GET http://10.0.0.24:8000/key/:users -H 'content-type: application/json' -H 'NS: test' -H 'DB: test' -H 'SC: allusers' -H 'Authorization: '+$token
{"code":403,"details":"Authentication failed","description":"Your authentication details are invalid. Reauthenticate using valid authentication parameters.","information":"There was a problem with authentication"}

@georgyo
Copy link

georgyo commented Sep 28, 2022

You need to prefix the token with Bearer So your header would be

-H "Authorization: Bearer ${token}"

This is true for almost all tokens including JWT and oauth.

@sublayerio
Copy link

Helpful thanks!

@bit-garden
Copy link

You need to prefix the token with Bearer So your header would be

-H "Authorization: Bearer ${token}"

This is true for almost all tokens including JWT and oauth.

Probably a point to make is that it needs double quotes specifically. Single quotes seems to throw a fit.

@maksimKadochnikov
Copy link

Hi, is it possible to generate JWT token in 3rd party service? Maybe i can access secret that SurrealDB uses to sign JWTs?

@koakh
Copy link
Author

koakh commented Oct 3, 2022

I don't understand the question, can elaborate?
Thanks

@maksimKadochnikov
Copy link

Of course, I am implementing an application that uses custom authentication. I receive from the client some data and its hash generated from another application, and I can validate this data using secret and send back a jwt token so after the client can interact with my application API. I want to use SurrealDB as an api/storage layer. Maybe you know if it is possible to implement it somehow? Perhaps I can inject this logic into the SurrealDB authentication flow, or somehow gain access to the secret that SurrealDB uses to sign JWT tokens, and do it using my custom service (e.g. lambda, etc.)

Here is also a pseudo-code implementation of what I'm talking about

function login(req) {
  // ?query_id=AAH9mUo3AAAAAP2ZSjcRGoav
  // &user={"id":927635965,"first_name":"john","last_name":"doe","username":"johndoe","language_code":"en"}
  // &auth_date=1664822318
  // &hash=0f559cc48df6d03f76a332a840b43e05ee62b41caf6325979eb5b31b5fb33525
  const { auth_date, query_id, user, hash } = req.query;

  if (auth_date && query_id && user && hash) {
    const data = "auth_date=" + auth_date + "\nquery_id=" + query_id + "\nuser=" + user;
    const secret = "some-secret";
    // sign data with secret
    const secretKeyHash = hmacSHA256(data, secret).toString();

    // compare hash with secretKeyHash
    if (secretKeyHash.toString() === hash) {
      // find user in db
      const apiUser = getUserFromDB(user);
      if (apiUser == "OK") {
        // return token for future API requests
        const token = jwt.sign({ sub: apiUser.id }, "SomeSecretToSignJWT", { expiresIn: '1d' })
        return token;
      } else {
        return "Some auth error";
      }
    }
}

@ralyodio
Copy link

ralyodio commented Oct 8, 2022

How do I get a user object from a jwt token?

@sublayerio
Copy link

How do I get a user object from a jwt token?

@ralyodio it's a json web token, you can decode it here https://jwt.io/

or use a package https://www.npmjs.com/package/jsonwebtoken

@koakh
Copy link
Author

koakh commented Oct 15, 2022

@wpitallo
Copy link

wpitallo commented Nov 4, 2022

Any way to use a third party authentication provider like google or git hub or authO

@koakh
Copy link
Author

koakh commented Nov 5, 2022 via email

@mysticaltech
Copy link

@wpitallo Have a look at https://authorizer.dev/. they will soon be supporting SurrealDB, and any help they can get would be welcome!

@alt-jero
Copy link

alt-jero commented Jan 2, 2023

I have a couple of questions. I run the setup, the SQL Part seems to work fine, but with JS I get an error: “There was a problem with authentication”. First Question, do you have to be signed in as root for the signup to work? So:

db.signin({user:root,pass:root})
db.signup({
  NS: 'my_ns',
  DB: 'my_db',
  SC: 'allusers', // We want to signup to the 'allusers' scope defined above
  email: 'test@acme.com', // We can add any variable here to use in the SIGNUP clause
  pass: 'some password', // We can add any variable here to use in the SIGNUP clause
  marketing: true, // We can add any variable here to use in the SIGNUP clause
  tags: ['rust', 'golang', 'javascript'], // We can add any variable here to use in the SIGNUP clause
});

Because in my case this didn't work either And with the namespaces, do you just need to do the SQL setup one time for all namespaces and db's ? Or is the SQL setup scoped to namespace or DB? And is there some deeper Documentation available for SurrealDB on the Documentation Page I found nothing about the setup to get signup working.

I have encountered the same problem as you did. The solution for me, as they often are, was a facepalm moment.

Are you ready?

  • Solution 1: Turn off strict mode. (Do this by omitting the --strict flag when starting the server)
  • Solution 2: Manually (as root) define the namespace, the database, and the table you used in the scope definition for signup and signin.

🤦

Unfortunately I don't know how to get the database to emit errors like this... if I knew rust I would be adding some pull requests, but alas at this point my skill is mostly node-based, and my focus is on my project. Anyway, I hope it helps the next one who comes here looking for:

  • There was a problem with authentication
  • surrealdb signup not working
  • surreal signup error

and other such search terms 😸

@hgoona
Copy link

hgoona commented Jan 17, 2023

I'm getting my head around scopes still and how to use them. I can see SC: allusers written into the request for Signin and Signup which is using the Root user and password, but what happens if someone uses those priveledges of Root and changes the SC: to something like an admin scope?

How is that prevented on the front-end app?? I'm a bit confused what parts need to be exposed and what parts are safe..

@kubeworkz
Copy link

kubeworkz commented Apr 17, 2023

@dustsucker did you ever get dynamic namespace/database creation working? I also need to create a namespace/database for each new user logging into the system. It seems like a trivial exercise but I can't seem to find a clear solution to this. Basically, a login flow like Supabase. Login -> Create a Project (new namespace/db in our case) and you're in. Extra bonus points for building a connection string so people can access SDB from the net.

@bitdom8
Copy link

bitdom8 commented Jun 29, 2023

@koakh hi, can you help us with argon2 https://github.com/orgs/surrealdb/discussions/2196#discussioncomment-6314556

Thanks for the great guidance

@pkastoori
Copy link

pkastoori commented Jul 4, 2023

I am using the signup function and providing a username and password. After that I use the signin function with the same username but with the a different password, I was expecting an error but I still got a token also calling the authenticate function after that did not throw an error. How is this possible? What am I doing wrong?

await db.signup({
NS: "mydb",
DB: "mydb",
SC: "allusers",
user: "jane",
password: "jane",
email: "jane@gmail.com",
});

const token = await db.signin({
NS: "mydb",
DB: "mydb",
SC: "allusers",
user: "jane",
password: "jane1",
});

await db.authenticate(token)

All of this still works even though I give an incorrect pass while signin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment