Verify programmatically created account and disable registration?

567 views
Skip to first unread message

Sam Bauch

unread,
Oct 23, 2020, 1:34:26 PM10/23/20
to Rodauth
I'm looking for a little help thinking through how we can balance security and UX for an open source web app.

When our users deploy an application, we want them to be able to use it. But we don't necessarily want open registration - we'll want our first user to be in control through some sort of invite mechanism or by programmatically creating accounts.

I think the best approach might be to use some combination of login, verify_account, and set password on verify?

So after deploy thought we might create an account programmatically using an email address. Then email that user to verify the account and add a password.

But it seems like the verify account depends on account create? It does seem I can verify programmatically created accounts, but even just enabling the feature adds the create-account route and form.

Alternatively perhaps we could use create_account, but would it be possible to only allow that functionality if there are 0 users in the DB?

I suppose we could also follow admin verification guide, and keep registration open? 

Anyhow, feels like a great project, hoping we can figure out the best way to approach this! Thanks in advance for any ideas

Jeremy Evans

unread,
Oct 23, 2020, 1:56:10 PM10/23/20
to rod...@googlegroups.com
On Fri, Oct 23, 2020 at 10:34 AM Sam Bauch <s...@enterpriseoss.dev> wrote:
I'm looking for a little help thinking through how we can balance security and UX for an open source web app.

When our users deploy an application, we want them to be able to use it. But we don't necessarily want open registration - we'll want our first user to be in control through some sort of invite mechanism or by programmatically creating accounts.

I think the best approach might be to use some combination of login, verify_account, and set password on verify?

So after deploy thought we might create an account programmatically using an email address. Then email that user to verify the account and add a password.

But it seems like the verify account depends on account create? It does seem I can verify programmatically created accounts, but even just enabling the feature adds the create-account route and form.

If you want the create-account route to be hidden, you can halt route processing before the route is taken by doing:

before_create_account_route do
  request.halt
end
 
Alternatively perhaps we could use create_account, but would it be possible to only allow that functionality if there are 0 users in the DB?

Similar idea here, only allowing the route if there are no accounts.

before_create_account_route do
  request.halt unless db[:accounts].empty?
end
 
I suppose we could also follow admin verification guide, and keep registration open? 

Anyhow, feels like a great project, hoping we can figure out the best way to approach this! Thanks in advance for any ideas

Hopefully the above ideas help.  Rodauth is pretty flexible, you should be able to override whatever parts you need.  If you have additional questions, please ask.

Thanks,
Jeremy

Shreko

unread,
Oct 26, 2020, 3:12:39 PM10/26/20
to Rodauth
I had a similar thought of what I would like to see in rodauth functionality. What about apps where only internal (company) users are allowed, would it make sense to sign up first user regular way, who would become administrator, then lock it for any other public signup and allow some kind of sign up by invitation only, where admin would generate and send invites who can sign up.

Thanks

Jeremy Evans

unread,
Oct 26, 2020, 3:34:23 PM10/26/20
to rod...@googlegroups.com
On Mon, Oct 26, 2020 at 12:12 PM Shreko <shre...@gmail.com> wrote:
I had a similar thought of what I would like to see in rodauth functionality. What about apps where only internal (company) users are allowed, would it make sense to sign up first user regular way, who would become administrator, then lock it for any other public signup and allow some kind of sign up by invitation only, where admin would generate and send invites who can sign up.

I recommend instead a way to setup an admin via the command line.  I consider it a bad idea to design a system that allows unauthorized admin access via a race condition.

Rodauth does not ship with an invitation feature, because that would first require it separate admins from regular users, and Rodauth deliberately doesn't do that.  Rodauth is for authentication, not authorization.  In my experience, it's fairly simple to create accounts automatically and email the user, and best left up to the application to do that.

Thanks,
Jeremy

Sam Bauch

unread,
Oct 27, 2020, 4:16:01 PM10/27/20
to Rodauth
Thanks Jeremy, always a pleasure to use your software and receive such nice support!

Thats a nice way to shut down registration after user 0.

I've made some progress here, but it seems like the verify_account feature doesn't have public methods for verifying the account when it was created programmatically?

I've got a little graphql mutation to invite a user - 

def resolve(email:, role:, oauth_client_id: nil)
  admin_user = Osso::Models::Account.new({
    email: email,
    status_id: 1,
    role: role,
    oauth_client_id: oauth_client_id,
  })

  if admin_user.save
    context[:rodauth].account_from_login(email)
    context[:rodauth].send_verify_account_email

    return response_data(admin_user: admin_user)
  end

  response_error(admin_user.errors)
end

seems to work well enough, but then the emailed verification link doesn't include a full token, just the account-ID_.

> If you created this account, please go to http://localhost:9292/verify-account?key=b7a02621-2099-4304-b267-d587a6df43c4_ to verify the account.

I am creating a verification key myself and associating it with the account, but since the feature depends on the instance var @verify_account_key_value which doesn't have a public setter then the email won't use the DB value.

Would making the setup_account_verification method public be a reasonable change? Is there a better way to hook in to the after_create_account for programmatically created accounts? IIRC Sequel eschews AR-style callbacks, so I don't think using AR instead of Sequel here makes much of a difference.

Thanks again!

Jeremy Evans

unread,
Oct 27, 2020, 5:08:53 PM10/27/20
to rod...@googlegroups.com
On Tue, Oct 27, 2020 at 1:16 PM Sam Bauch <s...@enterpriseoss.dev> wrote:
Thanks Jeremy, always a pleasure to use your software and receive such nice support!

Thats a nice way to shut down registration after user 0.

I've made some progress here, but it seems like the verify_account feature doesn't have public methods for verifying the account when it was created programmatically?

I've got a little graphql mutation to invite a user - 

def resolve(email:, role:, oauth_client_id: nil)
  admin_user = Osso::Models::Account.new({
    email: email,
    status_id: 1,
    role: role,
    oauth_client_id: oauth_client_id,
  })

  if admin_user.save
    context[:rodauth].account_from_login(email)
    context[:rodauth].send_verify_account_email

    return response_data(admin_user: admin_user)
  end

  response_error(admin_user.errors)
end

seems to work well enough, but then the emailed verification link doesn't include a full token, just the account-ID_. 
> If you created this account, please go to http://localhost:9292/verify-account?key=b7a02621-2099-4304-b267-d587a6df43c4_ to verify the account.

I am creating a verification key myself and associating it with the account, but since the feature depends on the instance var @verify_account_key_value which doesn't have a public setter then the email won't use the DB value.

Would making the setup_account_verification method public be a reasonable change? Is there a better way to hook in to the after_create_account for programmatically created accounts? IIRC Sequel eschews AR-style callbacks, so I don't think using AR instead of Sequel here makes much of a difference.

I'm OK with switching setup_account_verification from private to public, I'll make that change.  It was private mostly because there wasn't a need in Rodauth to make it public, not because I expect the implementation to change.

Thanks,
Jeremy

Sam Bauch

unread,
Oct 28, 2020, 4:41:38 PM10/28/20
to Rodauth
Ok great, thanks! 

I do realize i'm sorta shoehorning the verification feature into an invitation feature, so i appreciate the willingness to make that change!

I'll post the PR here when it's ready if anyone is curious / as an example / etc

Sam Bauch

unread,
Nov 3, 2020, 9:16:27 AM11/3/20
to Rodauth
some other things ended up sneaking into this, but here's the PR where we added Rodauth, replacing a JWT auth system we rolled ourselves - 


super pleased with how things turned out!

can we add ourselves to the list of projects using Rodauth?

Sam Bauch

unread,
Nov 3, 2020, 9:17:48 AM11/3/20
to Rodauth
sorry, thats the wrong(ish) link - most of the rodauth functionality is actually in our gem:

https://github.com/enterprise-oss/osso-rb/pull/34

Jeremy Evans

unread,
Nov 3, 2020, 10:06:58 AM11/3/20
to rod...@googlegroups.com
On Tue, Nov 3, 2020 at 6:16 AM Sam Bauch <s...@enterpriseoss.dev> wrote:
some other things ended up sneaking into this, but here's the PR where we added Rodauth, replacing a JWT auth system we rolled ourselves - 


super pleased with how things turned out!

can we add ourselves to the list of projects using Rodauth?

Sure, that would be great, please send a pull request for www/pages/documentation.erb

Thanks,
Jeremy

Sam Bauch

unread,
Nov 15, 2020, 12:37:29 PM11/15/20
to Rodauth
I've got one last task here that's seeming pretty difficult. Perhaps not, but I'm having a hard time following some of the meta-programming things. Admittedly this is also a bit outside the scope of Rodauth i think, I've gotten lucky so far to be able to get this far.

I want to now create a user and send a verification email in a Rake task, but things break down for me at the sending of the verification email where I'm unable to provide templating methods.

rodauth = Osso::Admin.rodauth.new(Class.new)
account = rodauth.account_from_login(admin_email)
rodauth.setup_account_verification

=> NoMethodError (undefined method `parse_template_opts' for #<Class:0x00007fbc7559f568>)

It feels like the scope arg to the Rodauth initializer wants an instance of a class that defines a bunch of templating methods. I'm using the Roda render plugin.

I've tried some things like this, but no matter what I've tried I can't get rodauth's scope to match whats going on in the Roda app itself, and haven't gotten emails to send.

class Fake
  include Roda::RodaPlugins::Render
  
  def initialize
    Roda::RodaPlugins::Render.configure(Osso::Admin)
  end
end

rodauth = Osso::Admin.rodauth.new(Fake.new)
account = rodauth.account_from_login(admin_email)
rodauth.setup_account_verification

Again, I totally understand that doing this all in a Rake task and outside of a request / response cycle is not what this functionality is intended for, but any tips to get this solved would be much appreciated! It does feel like the hash returned by  Roda::RodaPlugins::Render.configure(Osso::Admin) has what Rodauth needs to create the email template, but im missing something.

Jeremy Evans

unread,
Nov 15, 2020, 1:21:00 PM11/15/20
to rod...@googlegroups.com
On Sun, Nov 15, 2020 at 9:37 AM Sam Bauch <s...@enterpriseoss.dev> wrote:
I've got one last task here that's seeming pretty difficult. Perhaps not, but I'm having a hard time following some of the meta-programming things. Admittedly this is also a bit outside the scope of Rodauth i think, I've gotten lucky so far to be able to get this far.

I want to now create a user and send a verification email in a Rake task, but things break down for me at the sending of the verification email where I'm unable to provide templating methods.

rodauth = Osso::Admin.rodauth.new(Class.new)
account = rodauth.account_from_login(admin_email)
rodauth.setup_account_verification

=> NoMethodError (undefined method `parse_template_opts' for #<Class:0x00007fbc7559f568>)

It feels like the scope arg to the Rodauth initializer wants an instance of a class that defines a bunch of templating methods. I'm using the Roda render plugin.

Yes, it expects an instance of the Roda class.  You should not even attempt to get it to work without that.
 
I've tried some things like this, but no matter what I've tried I can't get rodauth's scope to match whats going on in the Roda app itself, and haven't gotten emails to send.

class Fake
  include Roda::RodaPlugins::Render
  
  def initialize
    Roda::RodaPlugins::Render.configure(Osso::Admin)
  end
end

rodauth = Osso::Admin.rodauth.new(Fake.new)
account = rodauth.account_from_login(admin_email)
rodauth.setup_account_verification

Again, I totally understand that doing this all in a Rake task and outside of a request / response cycle is not what this functionality is intended for, but any tips to get this solved would be much appreciated! It does feel like the hash returned by  Roda::RodaPlugins::Render.configure(Osso::Admin) has what Rodauth needs to create the email template, but im missing something.

Just create an instance of the Roda class and pass it in:

Osso::Admin.rodauth.new(Osso::Admin.new({})) # may need to populate some hash entries

Thanks,
Jeremy

Sam Bauch

unread,
Nov 16, 2020, 5:14:28 PM11/16/20
to Rodauth
Perfect!

Without any hash entries, both the sender domain and base url aren't set, which makes sense since those are read off of the request object.

I am already requiring BASE_URL as an env var, so i can specify those in the rodauth config block no problem:

plugin :rodauth do
  domain URI.parse(ENV['BASE_URL']).host
  base_url ENV['BASE_URL']
end

but I haven't yet figured out how I can pass values into this constructor that allow request.host and request.base_url to return the proper values. I tried

rodauth = Osso::Admin.rodauth.new(Osso::Admin.new({
  request: {
    base_url: ENV['BASE_URL'],
    host: URI.parse(ENV['BASE_URL']).host,
  }
}))

as well as using the rodauth auth_value_method name as the key like

rodauth = Osso::Admin.rodauth.new(Osso::Admin.new({
  base_url: ENV['BASE_URL'],
  domain: URI.parse(ENV['BASE_URL']).host,
}))

I'm happy with the rodauth block solution, but figured I'd ask.

Thanks much!

Jeremy Evans

unread,
Nov 16, 2020, 9:48:40 PM11/16/20
to rod...@googlegroups.com
On Mon, Nov 16, 2020 at 2:14 PM Sam Bauch <s...@enterpriseoss.dev> wrote:
Perfect!

Without any hash entries, both the sender domain and base url aren't set, which makes sense since those are read off of the request object.

I am already requiring BASE_URL as an env var, so i can specify those in the rodauth config block no problem:

plugin :rodauth do
  domain URI.parse(ENV['BASE_URL']).host
  base_url ENV['BASE_URL']
end

but I haven't yet figured out how I can pass values into this constructor that allow request.host and request.base_url to return the proper values. I tried

rodauth = Osso::Admin.rodauth.new(Osso::Admin.new({
  request: {
    base_url: ENV['BASE_URL'],
    host: URI.parse(ENV['BASE_URL']).host,
  }
}))

as well as using the rodauth auth_value_method name as the key like

rodauth = Osso::Admin.rodauth.new(Osso::Admin.new({
  base_url: ENV['BASE_URL'],
  domain: URI.parse(ENV['BASE_URL']).host,
}))

I'm happy with the rodauth block solution, but figured I'd ask.

Osso::Admin.new would take a rack env hash.  See  https://github.com/rack/rack/blob/master/SPEC.rdoc .  You probably want to at least pass in 'HTTP_HOST', 'PATH_INFO', and 'SCRIPT_NAME' keys.

Thanks,
Jeremy

Sam Bauch

unread,
Dec 22, 2020, 5:25:41 PM12/22/20
to Rodauth
I can't seem to figure out how to customize the email_from here.

rodauth = Osso::Admin.rodauth.new(Osso::Admin.new({
  'HTTP_HOST' => base_uri.host,
  'PATH_INFO' => '/',
  'SERVER_NAME' => base_uri.to_s,
  'rack.url_scheme' => base_uri.scheme,
}))

plugin :rodauth do
  enable :login, :verify_account
  email_from "Osso <no-reply@#{domain}>"
end

domain is always empty. are these methods from rodauth base not able to be called directly? the verification link in the email (which depends on base_url) works fine for me here. So what else do I need to do for request.host to return the host from request.base_uri? can I call the request object directly in my rodauth config block?

https://github.com/jeremyevans/rodauth/blob/master/lib/rodauth/features/base.rb#L445:L451

Jeremy Evans

unread,
Dec 22, 2020, 5:51:35 PM12/22/20
to rod...@googlegroups.com
On Tue, Dec 22, 2020 at 2:25 PM Sam Bauch <s...@enterpriseoss.dev> wrote:
I can't seem to figure out how to customize the email_from here.

rodauth = Osso::Admin.rodauth.new(Osso::Admin.new({
  'HTTP_HOST' => base_uri.host,
  'PATH_INFO' => '/',
  'SERVER_NAME' => base_uri.to_s,
  'rack.url_scheme' => base_uri.scheme,
}))

plugin :rodauth do
  enable :login, :verify_account
  email_from "Osso <no-reply@#{domain}>"

domain is an instance method in this case, so I'm guessing:

email_from{"Osso <no-reply@#{domain}>"}

Note that it's a good idea to set the base_url and domain in the rodauth configuration unless they will vary per request.  Otherwise, if the web server will handle a request for any domain name, you have a potential issue.

Thanks,
Jeremy
Reply all
Reply to author
Forward
0 new messages