Merge pull request #61 from simple-login/custom-alias-oauth-authorize

Custom alias oauth authorize
This commit is contained in:
Son Nguyen Kim 2020-01-30 00:04:22 +07:00 committed by GitHub
commit d1baca67ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 161 additions and 94 deletions

View file

@ -1,4 +1,4 @@
from flask import render_template, redirect, url_for, flash, request, session from flask import render_template, redirect, url_for, flash, request
from flask_login import login_required, current_user from flask_login import login_required, current_user
from app.config import ( from app.config import (
@ -9,7 +9,7 @@ from app.dashboard.base import dashboard_bp
from app.email_utils import email_belongs_to_alias_domains, get_email_domain_part from app.email_utils import email_belongs_to_alias_domains, get_email_domain_part
from app.extensions import db from app.extensions import db
from app.log import LOG from app.log import LOG
from app.models import GenEmail, CustomDomain from app.models import GenEmail, CustomDomain, DeletedAlias
from app.utils import convert_to_id, random_word, word_exist from app.utils import convert_to_id, random_word, word_exist
@ -49,7 +49,9 @@ def custom_alias():
): ):
full_alias = alias_prefix + alias_suffix full_alias = alias_prefix + alias_suffix
if GenEmail.get_by(email=full_alias): if GenEmail.get_by(email=full_alias) or DeletedAlias.get_by(
email=full_alias
):
LOG.d("full alias already used %s", full_alias) LOG.d("full alias already used %s", full_alias)
flash( flash(
f"Alias {full_alias} already exists, please choose another one", f"Alias {full_alias} already exists, please choose another one",

View file

@ -84,24 +84,42 @@
<div class="col-md-9"> <div class="col-md-9">
<select class="custom-select custom-select" name="suggested-email"> <select class="custom-select custom-select" name="suggested-email">
<option selected value="{{ suggested_email }}">{{ suggested_email }}</option> <option selected value="{{ suggested_email }}">{{ suggested_email }}</option>
<option value="{{ personal_email }}">{{ personal_email }} (Personal Email)</option> <option value="{{ current_user.email }}">{{ current_user.email }} (Personal Email)</option>
{% for email in other_emails %} {% for email in other_emails %}
<option value="{{ email }}">{{ email }}</option> <option value="{{ email }}">{{ email }}</option>
{% endfor %} {% endfor %}
</select> </select>
{% if current_user.can_create_new_alias() %} {% if current_user.can_create_new_alias() %}
<div class="mt-2">OR</div> <div class="mt-2 mb-2">OR</div>
<div style="display: flex; align-items: center" class="mt-2">
<input class="form-control" <div class="row mb-2">
pattern="[0-9a-z-_]{1,}" <div class="col-sm-6 pr-1 mb-1" style="min-width: 5em">
title="Only lowercase letter, number, dash (-), underscore (_) can be used in alias prefix." <input name="prefix" class="form-control"
style="flex-grow: 2" name="custom-email-prefix"> type="text"
<input type="hidden" name="email-suffix" value="{{ email_suffix }}"> pattern="[0-9a-z-_]{1,}"
<div class="ml-2"> title="Only lowercase letter, number, dash (-), underscore (_) can be used in alias prefix."
.{{ email_suffix }}@{{ EMAIL_DOMAIN }} placeholder="email alias"
autofocus>
</div>
<div class="col-sm-6"
style="padding-left: 5px">
<select class="form-control" name="suffix">
{% for suffix in suffixes %}
<option value="{{ suffix[1] }}">
{% if suffix[0] %}
{{ suffix[1] }} (your domain)
{% else %}
{{ suffix[1] }}
{% endif %}
</option>
{% endfor %}
</select>
</div> </div>
</div> </div>
<small class="text-muted"> <small class="text-muted">
Alias can use letter, number, dash and cannot be empty Alias can use letter, number, dash and cannot be empty
</small> </small>

View file

@ -1,47 +1,64 @@
{% extends "single.html" %} {% extends "base.html" %}
{% block single_content %} {% block content %}
<div class="row"> <div class="bg-white p-6" style="margin: auto; max-width: 600px">
<b>{{ client.name }}</b> &nbsp; would like to have access to your following data: <div class="text-center mb-6">
<a href="https://simplelogin.io">
<ul class="mt-3"> <img src="/static/logo.png" style="background-color: transparent; height: 40px">
{% for scope in client.get_scopes() %}
<li>
{% if scope == Scope.AVATAR_URL %}
avatar
{% else %}
{{ scope.value }}
{% endif %}
</li>
{% endfor %}
</ul>
<label>
In order to accept the request, you need to sign in.
</label>
</div>
<div class="row mt-4">
<div class="btn-group w-100">
<a href="{{ url_for('auth.login', next=next) }}" class="btn btn-success">
Login
</a>
<a href="{{ url_for('auth.register', next=next) }}" class="btn btn-info">
Sign Up
</a> </a>
</div> </div>
</div>
<hr> <div>
<b>{{ client.name }}</b> would like to have access to your following data:
</div>
<div>
<ul class="mt-3">
{% for scope in client.get_scopes() %}
<li>
{% if scope == Scope.AVATAR_URL %}
avatar
{% else %}
{{ scope.value }}
{% endif %}
</li>
{% endfor %}
</ul>
</div>
<div>
In order to accept the request, you need to sign in.
</div>
<div class="mt-4">
<div class="btn-group w-100">
<a href="{{ url_for('auth.login', next=next) }}" class="btn btn-success">
Login
</a>
<a href="{{ url_for('auth.register', next=next) }}" class="btn btn-info">
Sign Up
</a>
</div>
</div>
<hr>
<div class="">
<p class="text-center col">Cancel and go back to <b>{{ client.name }}</b></p>
<a class="btn btn-block btn-secondary back-or-close">
<i class="fe fe-arrow-left mr-2"></i>Cancel
</a>
</div>
<div class="small-text mt-4">
<a href="https://simplelogin.io">SimpleLogin</a> is an open source social login provider that protects your privacy.
</div>
<div class="row">
<p class="text-center col">Cancel and go back to <b>{{ client.name }}</b></p>
<a class="btn btn-block btn-secondary back-or-close">
<i class="fe fe-arrow-left mr-2"></i>Cancel
</a>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -4,7 +4,8 @@ from urllib.parse import urlparse
from flask import request, render_template, redirect, flash from flask import request, render_template, redirect, flash
from flask_login import current_user from flask_login import current_user
from app.config import EMAIL_DOMAIN from app.config import EMAIL_DOMAIN, ALIAS_DOMAINS, DISABLE_ALIAS_SUFFIX
from app.email_utils import get_email_domain_part
from app.extensions import db from app.extensions import db
from app.jose_utils import make_id_token from app.jose_utils import make_id_token
from app.log import LOG from app.log import LOG
@ -16,6 +17,7 @@ from app.models import (
RedirectUri, RedirectUri,
OauthToken, OauthToken,
DeletedAlias, DeletedAlias,
CustomDomain,
) )
from app.oauth.base import oauth_bp from app.oauth.base import oauth_bp
from app.oauth_models import ( from app.oauth_models import (
@ -103,21 +105,33 @@ def authorize():
client.name client.name
) )
suggested_name, other_names = current_user.suggested_names() suggested_name, other_names = current_user.suggested_names()
email_suffix = random_word()
user_custom_domains = [
cd.domain for cd in current_user.verified_custom_domains()
]
# List of (is_custom_domain, alias-suffix)
suffixes = []
# put custom domain first
for alias_domain in user_custom_domains:
suffixes.append((True, "@" + alias_domain))
# then default domain
for domain in ALIAS_DOMAINS:
suffixes.append(
(
False,
("" if DISABLE_ALIAS_SUFFIX else "." + random_word())
+ "@"
+ domain,
)
)
return render_template( return render_template(
"oauth/authorize.html", "oauth/authorize.html",
client=client,
user_info=user_info,
client_user=client_user,
Scope=Scope, Scope=Scope,
suggested_email=suggested_email,
personal_email=current_user.email,
suggested_name=suggested_name,
other_names=other_names,
other_emails=other_emails,
email_suffix=email_suffix,
EMAIL_DOMAIN=EMAIL_DOMAIN, EMAIL_DOMAIN=EMAIL_DOMAIN,
**locals(),
) )
else: else:
# after user logs in, redirect user back to this page # after user logs in, redirect user back to this page
@ -127,7 +141,7 @@ def authorize():
next=request.url, next=request.url,
Scope=Scope, Scope=Scope,
) )
else: # user allows or denies else: # POST - user allows or denies
if request.form.get("button") == "deny": if request.form.get("button") == "deny":
LOG.debug("User %s denies Client %s", current_user, client) LOG.debug("User %s denies Client %s", current_user, client)
final_redirect_uri = f"{redirect_uri}?error=deny&state={state}" final_redirect_uri = f"{redirect_uri}?error=deny&state={state}"
@ -140,32 +154,55 @@ def authorize():
if client_user: if client_user:
LOG.d("user %s has already allowed client %s", current_user, client) LOG.d("user %s has already allowed client %s", current_user, client)
else: else:
email_suffix = request.form.get("email-suffix") alias_prefix = request.form.get("prefix")
custom_email_prefix = request.form.get("custom-email-prefix") alias_suffix = request.form.get("suffix")
chosen_email = request.form.get("suggested-email")
suggested_name = request.form.get("suggested-name")
custom_name = request.form.get("custom-name")
use_default_avatar = request.form.get("avatar-choice") == "default"
gen_email = None gen_email = None
if custom_email_prefix:
# check if user can generate custom email # user creates a new alias, not using suggested alias
if alias_prefix:
# should never happen as this is checked on the front-end
if not current_user.can_create_new_alias(): if not current_user.can_create_new_alias():
raise Exception(f"User {current_user} cannot create custom email") raise Exception(f"User {current_user} cannot create custom email")
email = f"{convert_to_id(custom_email_prefix)}.{email_suffix}@{EMAIL_DOMAIN}" user_custom_domains = [
LOG.d("create custom email alias %s for user %s", email, current_user) cd.domain for cd in current_user.verified_custom_domains()
]
if GenEmail.get_by(email=email) or DeletedAlias.get_by(email=email): from app.dashboard.views.custom_alias import verify_prefix_suffix
LOG.error("email %s already used, very rare!", email)
flash(f"alias {email} already used", "error") if verify_prefix_suffix(
current_user, alias_prefix, alias_suffix, user_custom_domains
):
full_alias = alias_prefix + alias_suffix
if GenEmail.get_by(email=full_alias) or DeletedAlias.get_by(
email=full_alias
):
LOG.error("alias %s already used, very rare!", full_alias)
flash(f"Alias {full_alias} already used", "error")
return redirect(request.url)
else:
gen_email = GenEmail.create(
user_id=current_user.id, email=full_alias
)
# get the custom_domain_id if alias is created with a custom domain
alias_domain = get_email_domain_part(full_alias)
custom_domain = CustomDomain.get_by(domain=alias_domain)
if custom_domain:
gen_email.custom_domain_id = custom_domain.id
db.session.flush()
flash(f"Alias {full_alias} has been created", "success")
# only happen if the request has been "hacked"
else:
flash("something went wrong", "warning")
return redirect(request.url) return redirect(request.url)
# User chooses one of the suggestions
gen_email = GenEmail.create(email=email, user_id=current_user.id) else:
db.session.flush() chosen_email = request.form.get("suggested-email")
else: # user picks an email from suggestion # todo: add some checks on chosen_email
if chosen_email != current_user.email: if chosen_email != current_user.email:
gen_email = GenEmail.get_by(email=chosen_email) gen_email = GenEmail.get_by(email=chosen_email)
if not gen_email: if not gen_email:
@ -174,6 +211,11 @@ def authorize():
) )
db.session.flush() db.session.flush()
suggested_name = request.form.get("suggested-name")
custom_name = request.form.get("custom-name")
use_default_avatar = request.form.get("avatar-choice") == "default"
client_user = ClientUser.create( client_user = ClientUser.create(
client_id=client.id, user_id=current_user.id client_id=client.id, user_id=current_user.id
) )
@ -181,20 +223,8 @@ def authorize():
client_user.gen_email_id = gen_email.id client_user.gen_email_id = gen_email.id
if custom_name: if custom_name:
LOG.d(
"use custom name %s for user %s client %s",
custom_name,
current_user,
client,
)
client_user.name = custom_name client_user.name = custom_name
elif suggested_name != current_user.name: elif suggested_name != current_user.name:
LOG.d(
"use another name %s for user %s client %s",
custom_name,
current_user,
client,
)
client_user.name = suggested_name client_user.name = suggested_name
if use_default_avatar: if use_default_avatar: