Skip to content

Commit a86e432

Browse files
committed
miniOrange WordPress plugin authentication method; trap exception if Google service account JSON is malformed
1 parent 45e8900 commit a86e432

File tree

13 files changed

+119
-21
lines changed

13 files changed

+119
-21
lines changed

CHANGELOG.md

+17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# Change Log
22

3+
## [1.6.4] - 2025-03-01
4+
5+
### Added
6+
- The `allow external auth with multiple methods` Configuration
7+
directive.
8+
- The miniOrange WordPress plugin external authentication method.
9+
10+
### Changed
11+
- Users who registered with one external authentication method cannot
12+
log in using a different authentication method connected to the same
13+
email address unless the `allow external auth with multiple methods`
14+
Configuration directive is set to `True`.
15+
16+
### Fixed
17+
- Trapped exception where service account credentials contain invalid
18+
JSON.
19+
320
## [1.6.3] - 2025-02-16
421

522
### Fixed

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ bash -c \
5454
&& cp /usr/local/bin/unoconv /usr/bin/unoconv \
5555
&& python3.10 -m venv --copies /usr/share/docassemble/local3.10 \
5656
&& source /usr/share/docassemble/local3.10/bin/activate \
57-
&& pip install --upgrade pip==24.3.1 \
57+
&& pip install --upgrade pip==25.0.1 \
5858
&& pip install --upgrade wheel==0.45.1 \
5959
&& pip install --upgrade mod_wsgi==5.0.2 \
6060
&& pip install --upgrade \

LICENSE.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2015-2023 Jonathan Pyle
3+
Copyright (c) 2015-2025 Jonathan Pyle
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

docassemble/LICENSE.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2015-2023 Jonathan Pyle
3+
Copyright (c) 2015-2025 Jonathan Pyle
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

docassemble_base/LICENSE.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2015-2022 Jonathan Pyle
3+
Copyright (c) 2015-2025 Jonathan Pyle
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

docassemble_base/docassemble/base/data/sources/base-words.yml

+4
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,7 @@
747747
"Please enter the verification code from the text message we just sent you.": Null
748748
"Please enter the verification code from your authentication app.": Null
749749
"Please log in.": Null
750+
"Please log in to that account.": Null
750751
"Please select at least %s.": Null
751752
"Please select exactly %s.": Null
752753
"Please select no more than %s.": Null
@@ -816,6 +817,7 @@
816817
"Register with Azure": Null
817818
"Register with Facebook": Null
818819
"Register with Google": Null
820+
"Register with miniOrange": Null
819821
"Register with Twitter": Null
820822
"Register with your mobile phone": Null
821823
"Register with Zitadel": Null
@@ -921,6 +923,7 @@
921923
"Sign in with Facebook": Null
922924
"Sign in with Google": Null
923925
"Sign in with Keycloak": Null
926+
"Sign in with miniOrange": Null
924927
"Sign in with Twitter": Null
925928
"Sign in with your mobile phone": Null
926929
"Sign in with Zitadel": Null
@@ -1042,6 +1045,7 @@
10421045
"There are no active sessions.": Null
10431046
"There are unsaved changes. Are you sure you wish to leave this page?": Null
10441047
"The regular expression you provided could not be parsed.": Null
1048+
"There is already an account on the system with the e-mail address": Null
10451049
"There is already a username and password on this system with the e-mail address": Null
10461050
"The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.": Null
10471051
"There was an error.": Null

docassemble_demo/LICENSE.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2015-2023 Jonathan Pyle
3+
Copyright (c) 2015-2025 Jonathan Pyle
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

docassemble_webapp/LICENSE.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License (MIT)
22

3-
Copyright (c) 2015-2023 Jonathan Pyle
3+
Copyright (c) 2015-2025 Jonathan Pyle
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

docassemble_webapp/docassemble/webapp/google_api.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import sys
12
import logging
23
import json
34
from google.oauth2 import service_account
@@ -13,7 +14,12 @@
1314
if credential_json is None:
1415
credential_info = None
1516
else:
16-
credential_info = json.loads(credential_json, strict=False)
17+
try:
18+
credential_info = json.loads(credential_json, strict=False)
19+
except Exception as err:
20+
credential_info = None
21+
sys.stderr.write("Unable to load google service account credentials:\n")
22+
sys.stderr.write(str(err) + "\n")
1723

1824

1925
def google_api_credentials(scope):

docassemble_webapp/docassemble/webapp/server.py

+73-12
Original file line numberDiff line numberDiff line change
@@ -1036,7 +1036,7 @@ def custom_login():
10361036
if app.config['AUTO_LOGIN'] is True:
10371037
number_of_methods = 0
10381038
the_method = None
1039-
for login_method in ('USE_PHONE_LOGIN', 'USE_GOOGLE_LOGIN', 'USE_FACEBOOK_LOGIN', 'USE_ZITADEL_LOGIN', 'USE_TWITTER_LOGIN', 'USE_AUTH0_LOGIN', 'USE_KEYCLOAK_LOGIN', 'USE_AZURE_LOGIN'):
1039+
for login_method in ('USE_PHONE_LOGIN', 'USE_GOOGLE_LOGIN', 'USE_FACEBOOK_LOGIN', 'USE_ZITADEL_LOGIN', 'USE_TWITTER_LOGIN', 'USE_AUTH0_LOGIN', 'USE_KEYCLOAK_LOGIN', 'USE_AZURE_LOGIN', 'USE_MINIORANGE_LOGIN'):
10401040
if app.config[login_method]:
10411041
number_of_methods += 1
10421042
the_method = re.sub(r'USE_(.*)_LOGIN', r'\1', login_method).lower()
@@ -1048,7 +1048,7 @@ def custom_login():
10481048
return redirect(url_for('phone_login'))
10491049
if the_method == 'google':
10501050
return redirect(url_for('google_page', next=request.args.get('next', '')))
1051-
if the_method in ('facebook', 'twitter', 'auth0', 'keycloak', 'azure'):
1051+
if the_method in ('facebook', 'twitter', 'auth0', 'keycloak', 'azure', 'zitadel', 'miniorange'):
10521052
return redirect(url_for('oauth_authorize', provider=the_method, next=request.args.get('next', '')))
10531053
response = make_response(user_manager.render_function(user_manager.login_template,
10541054
form=login_form,
@@ -1269,6 +1269,11 @@ def url_for_interview(**args):
12691269

12701270
app.jinja_env.globals.update(url_for=url_for, url_for_interview=url_for_interview)
12711271

1272+
if DEBUG_BOOT:
1273+
boot_log("server: setting up logging")
1274+
1275+
sys_logger = None
1276+
12721277

12731278
def syslog_message(message):
12741279
message = re.sub(r'\n', ' ', message)
@@ -1299,12 +1304,6 @@ def syslog_message(message):
12991304
def syslog_message_with_timestamp(message):
13001305
syslog_message(time.strftime("%Y-%m-%d %H:%M:%S") + " " + message)
13011306

1302-
if DEBUG_BOOT:
1303-
boot_log("server: setting up logging")
1304-
1305-
sys_logger = logging.getLogger('docassemble')
1306-
sys_logger.setLevel(logging.DEBUG)
1307-
13081307
LOGFORMAT = daconfig.get('log format', 'docassemble: ip=%(clientip)s i=%(yamlfile)s uid=%(session)s user=%(user)s %(message)s')
13091308

13101309

@@ -1322,9 +1321,10 @@ def add_log_handler():
13221321
sys_logger.addHandler(stderr_log_handler)
13231322
break
13241323

1325-
add_log_handler()
1326-
13271324
if not (in_celery or in_cron):
1325+
sys_logger = logging.getLogger('docassemble')
1326+
sys_logger.setLevel(logging.DEBUG)
1327+
add_log_handler()
13281328
if LOGSERVER is None:
13291329
docassemble.base.logger.set_logmessage(syslog_message_with_timestamp)
13301330
else:
@@ -1505,6 +1505,7 @@ def get_twilio_config():
15051505
app.config['USE_GOOGLE_LOGIN'] = False
15061506
app.config['USE_FACEBOOK_LOGIN'] = False
15071507
app.config['USE_ZITADEL_LOGIN'] = False
1508+
app.config['USE_MINIORANGE_LOGIN'] = False
15081509
app.config['USE_TWITTER_LOGIN'] = False
15091510
app.config['USE_AUTH0_LOGIN'] = False
15101511
app.config['USE_KEYCLOAK_LOGIN'] = False
@@ -1522,6 +1523,7 @@ def get_twilio_config():
15221523
app.config['USE_GOOGLE_LOGIN'] = bool('google' in daconfig['oauth'] and not ('enable' in daconfig['oauth']['google'] and daconfig['oauth']['google']['enable'] is False))
15231524
app.config['USE_FACEBOOK_LOGIN'] = bool('facebook' in daconfig['oauth'] and not ('enable' in daconfig['oauth']['facebook'] and daconfig['oauth']['facebook']['enable'] is False))
15241525
app.config['USE_ZITADEL_LOGIN'] = bool('zitadel' in daconfig['oauth'] and not ('enable' in daconfig['oauth']['zitadel'] and daconfig['oauth']['zitadel']['enable'] is False))
1526+
app.config['USE_MINIORANGE_LOGIN'] = bool('miniorange' in daconfig['oauth'] and not ('enable' in daconfig['oauth']['zitadel'] and daconfig['oauth']['miniorange']['enable'] is False))
15251527
app.config['USE_TWITTER_LOGIN'] = bool('twitter' in daconfig['oauth'] and not ('enable' in daconfig['oauth']['twitter'] and daconfig['oauth']['twitter']['enable'] is False))
15261528
app.config['USE_AUTH0_LOGIN'] = bool('auth0' in daconfig['oauth'] and not ('enable' in daconfig['oauth']['auth0'] and daconfig['oauth']['auth0']['enable'] is False))
15271529
app.config['USE_KEYCLOAK_LOGIN'] = bool('keycloak' in daconfig['oauth'] and not ('enable' in daconfig['oauth']['keycloak'] and daconfig['oauth']['keycloak']['enable'] is False))
@@ -3441,7 +3443,7 @@ def delete_session_sessions():
34413443

34423444

34433445
def delete_session_info():
3444-
for key in ('i', 'uid', 'key_logged', 'tempuser', 'user_id', 'encrypted', 'chatstatus', 'observer', 'monitor', 'variablefile', 'doing_sms', 'playgroundfile', 'playgroundtemplate', 'playgroundstatic', 'playgroundsources', 'playgroundmodules', 'playgroundpackages', 'taskwait', 'phone_number', 'otp_secret', 'validated_user', 'github_next', 'next', 'sessions', 'alt_session', 'zitadel_verifier'):
3446+
for key in ('i', 'uid', 'key_logged', 'tempuser', 'user_id', 'encrypted', 'chatstatus', 'observer', 'monitor', 'variablefile', 'doing_sms', 'playgroundfile', 'playgroundtemplate', 'playgroundstatic', 'playgroundsources', 'playgroundmodules', 'playgroundpackages', 'taskwait', 'phone_number', 'otp_secret', 'validated_user', 'github_next', 'next', 'sessions', 'alt_session', 'zitadel_verifier', 'miniorange_verifier'):
34453447
if key in session:
34463448
del session[key]
34473449

@@ -4721,6 +4723,9 @@ def __init__(self, provider_name):
47214723
def authorize(self):
47224724
pass
47234725

4726+
def enabled(self):
4727+
return app.config.get(f"USE_{self.provider_name.upper()}_LOGIN", False)
4728+
47244729
def callback(self):
47254730
pass
47264731

@@ -4918,6 +4923,57 @@ def callback(self):
49184923
)
49194924

49204925

4926+
class MiniOrangeOAuthSignIn(OAuthSignIn):
4927+
4928+
def __init__(self):
4929+
super().__init__('miniorange')
4930+
self.service = OAuth2Service(
4931+
name='azure',
4932+
client_id=self.consumer_id,
4933+
client_secret=self.consumer_secret,
4934+
authorize_url='https://' + str(self.consumer_domain) + '/wp-json/moserver/authorize',
4935+
access_token_url='https://' + str(self.consumer_domain) + '/wp-json/moserver/token',
4936+
base_url='https://' + str(self.consumer_domain)
4937+
)
4938+
4939+
def authorize(self):
4940+
session['miniorange_verifier'] = random_alphanumeric(43)
4941+
return redirect(self.service.get_authorize_url(
4942+
response_type='code',
4943+
client_id=self.consumer_id,
4944+
redirect_uri=self.get_callback_url(),
4945+
scope='openid profile email',
4946+
state=session['miniorange_verifier'])
4947+
)
4948+
4949+
def callback(self):
4950+
if 'code' not in request.args or 'miniorange_verifier' not in session:
4951+
return None, None, None, None
4952+
the_state = request.args.get('state', '')
4953+
if the_state != session['miniorange_verifier']:
4954+
del session['miniorange_verifier']
4955+
return None, None, None, None
4956+
the_data = {'code': request.args['code'],
4957+
'grant_type': 'authorization_code',
4958+
'client_id': self.consumer_id,
4959+
'client_secret': self.consumer_secret,
4960+
'redirect_uri': self.get_callback_url()}
4961+
oauth_session = self.service.get_auth_session(
4962+
decoder=safe_json_loads,
4963+
data=the_data
4964+
)
4965+
me = oauth_session.get('wp-json/moserver/resource').json()
4966+
del session['miniorange_verifier']
4967+
return (
4968+
'miniorange$' + str(me['id']),
4969+
me.get('email').split('@')[0],
4970+
me.get('email'),
4971+
{'first_name': me.get('first_name', None),
4972+
'last_name': me.get('last_name', None),
4973+
'name': (me.get('first_name', '') + ' ' + me.get('last_name', '')).strip()}
4974+
)
4975+
4976+
49214977
def safe_json_loads(data):
49224978
return json.loads(data.decode("utf-8", "strict"))
49234979

@@ -5174,6 +5230,8 @@ def oauth_callback(provider):
51745230
# for argument in request.args:
51755231
# logmessage("argument " + str(argument) + " is " + str(request.args[argument]))
51765232
oauth = OAuthSignIn.get_provider(provider)
5233+
if not oauth.enabled():
5234+
abort(403)
51775235
social_id, username, email, name_data = oauth.callback()
51785236
if not verify_email(email):
51795237
flash(word('E-mail addresses with this domain are not authorized to register for accounts on this system.'), 'error')
@@ -5183,7 +5241,10 @@ def oauth_callback(provider):
51835241
return redirect(url_for('interview_list', from_login='1'))
51845242
user = db.session.execute(select(UserModel).options(db.joinedload(UserModel.roles)).filter_by(social_id=social_id)).scalar()
51855243
if not user:
5186-
user = db.session.execute(select(UserModel).options(db.joinedload(UserModel.roles)).filter_by(email=email)).scalar()
5244+
user = db.session.execute(select(UserModel).options(db.joinedload(UserModel.roles)).where(UserModel.email.ilike(email))).scalar()
5245+
if user and not user.social_id.startswith('local') and not daconfig.get('allow external auth with multiple methods', False) and social_id.split('$')[0] != user.social_id.split('$')[0]:
5246+
flash(word('There is already an account on the system with the e-mail address') + " " + str(email) + ". " + word("Please log in to that account."), 'error')
5247+
return redirect(url_for('user.login'))
51875248
if user and user.social_id is not None and user.social_id.startswith('local'):
51885249
flash(word('There is already a username and password on this system with the e-mail address') + " " + str(email) + ". " + word("Please log in."), 'error')
51895250
return redirect(url_for('user.login'))
Loading

docassemble_webapp/docassemble/webapp/templates/flask_user/login.html

+6-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ <h1>{{ get_part('login page heading', word('Sign in')) }}</h1>
8383
{{ render_submit_field(form.submit) }}
8484
</div>
8585
</form>
86-
{%- if config['USE_GOOGLE_LOGIN'] or config['USE_FACEBOOK_LOGIN'] or config['USE_ZITADEL_LOGIN'] or config['USE_TWITTER_LOGIN'] or config['USE_AUTH0_LOGIN'] or config['USE_KEYCLOAK_LOGIN'] or config['USE_AZURE_LOGIN'] or config['USE_PHONE_LOGIN'] %}
86+
{%- if config['USE_GOOGLE_LOGIN'] or config['USE_FACEBOOK_LOGIN'] or config['USE_ZITADEL_LOGIN'] or config['USE_TWITTER_LOGIN'] or config['USE_AUTH0_LOGIN'] or config['USE_KEYCLOAK_LOGIN'] or config['USE_AZURE_LOGIN'] or config['USE_MINIORANGE_LOGIN'] or config['USE_PHONE_LOGIN'] %}
8787
<p style="padding: 15px 15px 5px 15px;"><strong>{{ word('or') }}</strong></p>
8888
{%- endif %}
8989
{%- endif %}
@@ -122,6 +122,11 @@ <h1>{{ get_part('login page heading', word('Sign in')) }}</h1>
122122
<div class="daiconbox col-md-7"><a role="button" class="danohover" href="{{ url_for('oauth_authorize', provider='zitadel', next=request.args.get('next', '')) }}"><table><tbody><tr><td style="padding-left:4px;vertical-align:middle;"><img alt="" src="{{ url_for('static', filename='app/zitadel-logo.png', v=config['DA_VERSION']) }}"></td><td style="width:100%;vertical-align:middle;text-align:center;">{{ word('Sign in with Zitadel') }}</td></tr></tbody></table></a></div>
123123
</div>
124124
{%- endif %}
125+
{%- if config['USE_MINIORANGE_LOGIN'] %}
126+
<div class="row danomargin">
127+
<div class="daiconbox col-md-7"><a role="button" class="danohover" href="{{ url_for('oauth_authorize', provider='miniorange', next=request.args.get('next', '')) }}"><table><tbody><tr><td style="padding-left:4px;vertical-align:middle;"><img alt="" src="{{ url_for('static', filename='app/miniorange-logo.png', v=config['DA_VERSION']) }}"></td><td style="width:100%;vertical-align:middle;text-align:center;">{{ word('Sign in with miniOrange') }}</td></tr></tbody></table></a></div>
128+
</div>
129+
{%- endif %}
125130
{%- if config['USE_AZURE_LOGIN'] %}
126131
<div class="row danomargin">
127132
<div class="daiconbox col-md-7"><a role="button" class="danohover" href="{{ url_for('oauth_authorize', provider='azure', next=request.args.get('next', '')) }}"><table style="height:100%"><tbody><tr><td style="padding-left:4px;vertical-align:middle;"><img alt="" src="{{ url_for('static', filename='app/azure-logo.png', v=config['DA_VERSION']) }}"></td><td style="width:100%;vertical-align:middle;text-align:center;">{{ word('Sign in with Azure') }}</td></tr></tbody></table></a></div>

docassemble_webapp/docassemble/webapp/templates/flask_user/register.html

+6-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ <h1>{{ get_part('register page heading', word('Register')) }}</h1>
6868
{{ render_submit_field(form.submit) }}
6969
</div>
7070
</form>
71-
{%- if config['USE_GOOGLE_LOGIN'] or config['USE_FACEBOOK_LOGIN'] or config['USE_ZITADEL_LOGIN'] or config['USE_TWITTER_LOGIN'] or config['USE_AUTH0_LOGIN'] or config['USE_AZURE_LOGIN'] or config['USE_PHONE_LOGIN'] %}
71+
{%- if config['USE_GOOGLE_LOGIN'] or config['USE_FACEBOOK_LOGIN'] or config['USE_ZITADEL_LOGIN'] or config['USE_TWITTER_LOGIN'] or config['USE_AUTH0_LOGIN'] or config['USE_AZURE_LOGIN'] or config['USE_MINIORANGE_LOGIN'] or config['USE_PHONE_LOGIN'] %}
7272
<p style="padding: 15px 15px 5px 15px;"><strong>{{ word('or') }}</strong></p>
7373
{%- endif %}
7474
{%- endif %}
@@ -102,6 +102,11 @@ <h1>{{ get_part('register page heading', word('Register')) }}</h1>
102102
<div class="daiconbox col-md-7"><a role="button" class="danohover" href="{{ url_for('oauth_authorize', provider='zitadel') }}"><table><tbody><tr><td style="padding-left:4px;vertical-align:middle;"><img alt="" src="{{ url_for('static', filename='app/zitadel-logo.png', v=config['DA_VERSION']) }}"></td><td style="width:100%;vertical-align:middle;text-align:center;">{{ word('Register with Zitadel') }}</td></tr></tbody></table></a></div>
103103
</div>
104104
{%- endif %}
105+
{%- if config['USE_MINIORANGE_LOGIN'] %}
106+
<div class="row danomargin">
107+
<div class="daiconbox col-md-7"><a role="button" class="danohover" href="{{ url_for('oauth_authorize', provider='miniorange') }}"><table><tbody><tr><td style="padding-left:4px;vertical-align:middle;"><img alt="" src="{{ url_for('static', filename='app/miniorange-logo.png', v=config['DA_VERSION']) }}"></td><td style="width:100%;vertical-align:middle;text-align:center;">{{ word('Register with miniOrange') }}</td></tr></tbody></table></a></div>
108+
</div>
109+
{%- endif %}
105110
{%- if config['USE_AZURE_LOGIN'] %}
106111
<div class="row danomargin">
107112
<div class="daiconbox col-md-7"><a role="button" class="danohover" href="{{ url_for('oauth_authorize', provider='azure') }}"><table style="height:100%"><tbody><tr><td style="padding-left:4px;vertical-align:middle;"><img alt="" src="{{ url_for('static', filename='app/azure-logo.png', v=config['DA_VERSION']) }}"></td><td style="width:100%;vertical-align:middle;text-align:center;">{{ word('Register with Azure') }}</td></tr></tbody></table></a></div>

0 commit comments

Comments
 (0)