def new_custom_alias(): """ Create a new custom alias Input: alias_prefix, for ex "www_groupon_com" alias_suffix, either [email protected] or @my-domain.com optional "hostname" in args optional "note" Output: 201 if success 409 if the alias already exists """ user: User = g.user if not user.can_create_new_alias(): LOG.d("user %s cannot create any custom alias", user) return ( jsonify( error="You have reached the limitation of a free account with the maximum of " f"{MAX_NB_EMAIL_FREE_PLAN} aliases, please upgrade your plan to create more aliases" ), 400, ) user_custom_domains = [cd.domain for cd in user.verified_custom_domains()] hostname = request.args.get("hostname") data = request.get_json() if not data: return jsonify(error="request body cannot be empty"), 400 alias_prefix = data.get("alias_prefix", "").strip() alias_suffix = data.get("alias_suffix", "").strip() note = data.get("note") alias_prefix = convert_to_id(alias_prefix) if not verify_prefix_suffix(user, alias_prefix, alias_suffix, user_custom_domains): return jsonify(error="wrong alias prefix or suffix"), 400 full_alias = alias_prefix + alias_suffix if GenEmail.get_by(email=full_alias): LOG.d("full alias already used %s", full_alias) return jsonify(error=f"alias {full_alias} already exists"), 409 gen_email = GenEmail.create( user_id=user.id, email=full_alias, mailbox_id=user.default_mailbox_id, note=note ) db.session.commit() if hostname: AliasUsedOn.create(gen_email_id=gen_email.id, hostname=hostname) db.session.commit() return jsonify(alias=full_alias), 201
def custom_alias(): # check if user has not exceeded the alias quota if not current_user.can_create_new_alias(): # notify admin LOG.error("user %s tries to create custom alias", current_user) flash("ony premium user can choose custom alias", "warning") return redirect(url_for("dashboard.index")) 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, ) ) if request.method == "POST": alias_prefix = request.form.get("prefix") alias_suffix = request.form.get("suffix") 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): LOG.d("full alias already used %s", full_alias) flash( f"Alias {full_alias} already exists, please choose another one", "warning", ) else: gen_email = GenEmail.create(user_id=current_user.id, email=full_alias) db.session.commit() flash(f"Alias {full_alias} has been created", "success") session[HIGHLIGHT_GEN_EMAIL_ID] = gen_email.id return redirect(url_for("dashboard.index")) # only happen if the request has been "hacked" else: flash("something went wrong", "warning") return render_template("dashboard/custom_alias.html", **locals())
def try_auto_create_directory(alias: str) -> Optional[GenEmail]: """ Try to create an alias with directory """ # check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format if email_belongs_to_alias_domains(alias): # if there's no directory separator in the alias, no way to auto-create it if "/" not in alias and "+" not in alias and "#" not in alias: return None # alias contains one of the 3 special directory separator: "/", "+" or "#" if "/" in alias: sep = "/" elif "+" in alias: sep = "+" else: sep = "#" directory_name = alias[: alias.find(sep)] LOG.d("directory_name %s", directory_name) directory = Directory.get_by(name=directory_name) if not directory: return None dir_user: User = directory.user if not dir_user.can_create_new_alias(): send_cannot_create_directory_alias(dir_user, alias, directory_name) return None # if alias has been deleted before, do not auto-create it if DeletedAlias.get_by(email=alias, user_id=directory.user_id): LOG.warning( "Alias %s was deleted before, cannot auto-create using directory %s, user %s", alias, directory_name, dir_user, ) return None LOG.d("create alias %s for directory %s", alias, directory) gen_email = GenEmail.create( email=alias, user_id=directory.user_id, directory_id=directory.id, mailbox_id=dir_user.default_mailbox_id, ) db.session.commit() return gen_email
def try_auto_create_catch_all_domain(alias: str) -> Optional[GenEmail]: """Try to create an alias with catch-all domain""" # try to create alias on-the-fly with custom-domain catch-all feature # check if alias is custom-domain alias and if the custom-domain has catch-all enabled alias_domain = get_email_domain_part(alias) custom_domain = CustomDomain.get_by(domain=alias_domain) if not custom_domain: return None # custom_domain exists if not custom_domain.catch_all: return None # custom_domain has catch-all enabled domain_user: User = custom_domain.user if not domain_user.can_create_new_alias(): send_cannot_create_domain_alias(domain_user, alias, alias_domain) return None # if alias has been deleted before, do not auto-create it if DeletedAlias.get_by(email=alias, user_id=custom_domain.user_id): LOG.warning( "Alias %s was deleted before, cannot auto-create using domain catch-all %s, user %s", alias, custom_domain, domain_user, ) return None LOG.d("create alias %s for domain %s", alias, custom_domain) gen_email = GenEmail.create( email=alias, user_id=custom_domain.user_id, custom_domain_id=custom_domain.id, automatic_creation=True, mailbox_id=domain_user.default_mailbox_id, ) db.session.commit() return gen_email
def handle_forward(self, envelope, smtp: SMTP, msg: Message) -> str: """return *status_code message*""" alias = envelope.rcpt_tos[0].lower() # alias@SL gen_email = GenEmail.get_by(email=alias) if not gen_email: LOG.d( "alias %s not exist. Try to see if it can be created on the fly", alias) # try to see if alias could be created on-the-fly on_the_fly = False # check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format if email_belongs_to_alias_domains(alias): if "/" in alias or "+" in alias or "#" in alias: if "/" in alias: sep = "/" elif "+" in alias: sep = "+" else: sep = "#" directory_name = alias[:alias.find(sep)] LOG.d("directory_name %s", directory_name) directory = Directory.get_by(name=directory_name) # Only premium user can use the directory feature if directory: dir_user = directory.user if dir_user.is_premium(): LOG.d("create alias %s for directory %s", alias, directory) on_the_fly = True gen_email = GenEmail.create( email=alias, user_id=directory.user_id, directory_id=directory.id, ) db.session.commit() else: LOG.error( "User %s is not premium anymore and cannot create alias with directory", dir_user, ) send_cannot_create_directory_alias( dir_user, alias, directory_name) # try to create alias on-the-fly with custom-domain catch-all feature # check if alias is custom-domain alias and if the custom-domain has catch-all enabled if not on_the_fly: alias_domain = get_email_domain_part(alias) custom_domain = CustomDomain.get_by(domain=alias_domain) # Only premium user can continue using the catch-all feature if custom_domain and custom_domain.catch_all: domain_user = custom_domain.user if domain_user.is_premium(): LOG.d("create alias %s for domain %s", alias, custom_domain) on_the_fly = True gen_email = GenEmail.create( email=alias, user_id=custom_domain.user_id, custom_domain_id=custom_domain.id, automatic_creation=True, ) db.session.commit() else: LOG.error( "User %s is not premium anymore and cannot create alias with domain %s", domain_user, alias_domain, ) send_cannot_create_domain_alias( domain_user, alias, alias_domain) if not on_the_fly: LOG.d("alias %s cannot be created on-the-fly, return 510", alias) return "510 Email not exist" user_email = gen_email.user.email website_email = get_email_part(msg["From"]) forward_email = ForwardEmail.get_by(gen_email_id=gen_email.id, website_email=website_email) if not forward_email: LOG.debug( "create forward email for alias %s and website email %s", alias, website_email, ) # generate a reply_email, make sure it is unique # not use while to avoid infinite loop for _ in range(1000): reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}" if not ForwardEmail.get_by(reply_email=reply_email): break forward_email = ForwardEmail.create( gen_email_id=gen_email.id, website_email=website_email, website_from=msg["From"], reply_email=reply_email, ) db.session.commit() forward_log = ForwardEmailLog.create(forward_id=forward_email.id) if gen_email.enabled: # add custom header add_or_replace_header(msg, "X-SimpleLogin-Type", "Forward") # remove reply-to header if present delete_header(msg, "Reply-To") # change the from header so the sender comes from @SL # so it can pass DMARC check # replace the email part in from: header from_header = (get_email_name(msg["From"]) + " - " + website_email.replace("@", " at ") + f" <{forward_email.reply_email}>") msg.replace_header("From", from_header) LOG.d("new from header:%s", from_header) # add List-Unsubscribe header unsubscribe_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}" add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>") add_or_replace_header(msg, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click") add_dkim_signature(msg, EMAIL_DOMAIN) LOG.d( "Forward mail from %s to %s, mail_options %s, rcpt_options %s ", website_email, user_email, envelope.mail_options, envelope.rcpt_options, ) # smtp.send_message has UnicodeEncodeErroremail issue # encode message raw directly instead msg_raw = msg.as_string().encode() smtp.sendmail( forward_email.reply_email, user_email, msg_raw, envelope.mail_options, envelope.rcpt_options, ) else: LOG.d("%s is disabled, do not forward", gen_email) forward_log.blocked = True db.session.commit() return "250 Message accepted for delivery"
def custom_alias(): # check if user has not exceeded the alias quota if not current_user.can_create_new_alias(): # notify admin LOG.error("user %s tries to create custom alias", current_user) flash( "You have reached free plan limit, please upgrade to create new aliases", "warning", ) return redirect(url_for("dashboard.index")) 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, ) ) if request.method == "POST": alias_prefix = request.form.get("prefix") alias_suffix = request.form.get("suffix") alias_note = request.form.get("note") 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.d("full alias already used %s", full_alias) flash( f"Alias {full_alias} already exists, please choose another one", "warning", ) else: gen_email = GenEmail.create( user_id=current_user.id, email=full_alias, note=alias_note ) # 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.commit() flash(f"Alias {full_alias} has been created", "success") return redirect( url_for("dashboard.index", highlight_gen_email_id=gen_email.id) ) # only happen if the request has been "hacked" else: flash("something went wrong", "warning") return render_template("dashboard/custom_alias.html", **locals())
def custom_alias(): # check if user has the right to create custom alias if not current_user.can_create_new_alias(): # notify admin LOG.error("user %s tries to create custom alias", current_user) flash("ony premium user can choose custom alias", "warning") return redirect(url_for("dashboard.index")) error = "" if request.method == "POST": if request.form.get("form-name") == "non-custom-domain-name": email_prefix = request.form.get("email-prefix") email_prefix = convert_to_id(email_prefix) email_suffix = request.form.get("email-suffix") if not email_prefix: error = "alias prefix cannot be empty" else: full_email = f"{email_prefix}.{email_suffix}@{EMAIL_DOMAIN}" # check if email already exists if GenEmail.get_by(email=full_email) or DeletedAlias.get_by( email=full_email): error = "email already chosen, please choose another one" else: # create the new alias LOG.d("create custom alias %s for user %s", full_email, current_user) gen_email = GenEmail.create(email=full_email, user_id=current_user.id) db.session.commit() flash(f"Alias {full_email} has been created", "success") session[HIGHLIGHT_GEN_EMAIL_ID] = gen_email.id return redirect(url_for("dashboard.index")) elif request.form.get("form-name") == "custom-domain-name": custom_domain_id = request.form.get("custom-domain-id") email = request.form.get("email") custom_domain = CustomDomain.get(custom_domain_id) if not custom_domain: flash("Unknown error. Refresh the page", "warning") return redirect(url_for("dashboard.custom_alias")) elif custom_domain.user_id != current_user.id: flash("Unknown error. Refresh the page", "warning") return redirect(url_for("dashboard.custom_alias")) elif not custom_domain.verified: flash("Unknown error. Refresh the page", "warning") return redirect(url_for("dashboard.custom_alias")) full_email = f"{email}@{custom_domain.domain}" if GenEmail.get_by(email=full_email): error = f"{full_email} already exist, please choose another one" else: LOG.d( "create custom alias %s for custom domain %s", full_email, custom_domain.domain, ) gen_email = GenEmail.create( email=full_email, user_id=current_user.id, custom_domain_id=custom_domain.id, ) db.session.commit() flash(f"Alias {full_email} has been created", "success") session[HIGHLIGHT_GEN_EMAIL_ID] = gen_email.id return redirect(url_for("dashboard.index")) email_suffix = random_word() return render_template( "dashboard/custom_alias.html", error=error, email_suffix=email_suffix, EMAIL_DOMAIN=EMAIL_DOMAIN, custom_domains=current_user.verified_custom_domains(), )
def handle_forward(self, envelope, smtp: SMTP, msg: EmailMessage) -> str: """return *status_code message*""" alias = envelope.rcpt_tos[0] # alias@SL gen_email = GenEmail.get_by(email=alias) if not gen_email: LOG.d("alias %s not exist") # check if alias is custom-domain alias and if the custom-domain has catch-all enabled alias_domain = get_email_domain_part(alias) custom_domain = CustomDomain.get_by(domain=alias_domain) if custom_domain and custom_domain.catch_all: LOG.d("create alias %s for domain %s", alias, custom_domain) gen_email = GenEmail.create( email=alias, user_id=custom_domain.user_id, custom_domain_id=custom_domain.id, automatic_creation=True, ) db.session.commit() else: return "510 Email not exist" user_email = gen_email.user.email website_email = get_email_part(msg["From"]) forward_email = ForwardEmail.get_by(gen_email_id=gen_email.id, website_email=website_email) if not forward_email: LOG.debug( "create forward email for alias %s and website email %s", alias, website_email, ) # generate a reply_email, make sure it is unique # not use while to avoid infinite loop for _ in range(1000): reply_email = f"reply+{random_string(30)}@{EMAIL_DOMAIN}" if not ForwardEmail.get_by(reply_email=reply_email): break forward_email = ForwardEmail.create( gen_email_id=gen_email.id, website_email=website_email, website_from=msg["From"], reply_email=reply_email, ) db.session.commit() forward_log = ForwardEmailLog.create(forward_id=forward_email.id) if gen_email.enabled: # add custom header add_or_replace_header(msg, "X-SimpleLogin-Type", "Forward") # remove reply-to header if present if msg["Reply-To"]: LOG.d("Delete reply-to header %s", msg["Reply-To"]) del msg["Reply-To"] # change the from header so the sender comes from @SL # so it can pass DMARC check # replace the email part in from: header from_header = (get_email_name(msg["From"]) + " - " + website_email.replace("@", " at ") + f" <{forward_email.reply_email}>") msg.replace_header("From", from_header) LOG.d("new from header:%s", from_header) # add List-Unsubscribe header unsubscribe_link = f"{URL}/dashboard/unsubscribe/{gen_email.id}" add_or_replace_header(msg, "List-Unsubscribe", f"<{unsubscribe_link}>") add_or_replace_header(msg, "List-Unsubscribe-Post", "List-Unsubscribe=One-Click") add_dkim_signature(msg, EMAIL_DOMAIN) LOG.d( "Forward mail from %s to %s, mail_options %s, rcpt_options %s ", website_email, user_email, envelope.mail_options, envelope.rcpt_options, ) # smtp.send_message has UnicodeEncodeErroremail issue # encode message raw directly instead msg_raw = msg.as_string().encode() smtp.sendmail( forward_email.reply_email, user_email, msg_raw, envelope.mail_options, envelope.rcpt_options, ) else: LOG.d("%s is disabled, do not forward", gen_email) forward_log.blocked = True db.session.commit() return "250 Message accepted for delivery"
def new_custom_alias(): """ Create a new custom alias Input: alias_prefix, for ex "www_groupon_com" alias_suffix, either [email protected] or @my-domain.com optional "hostname" in args Output: 201 if success 409 if alias already exists """ user = g.user if not user.can_create_new_alias(): LOG.d("user %s cannot create custom alias", user) return ( jsonify( error="You have created 3 custom aliases, please upgrade to create more" ), 400, ) user_custom_domains = [cd.domain for cd in user.verified_custom_domains()] hostname = request.args.get("hostname") data = request.get_json() alias_prefix = data["alias_prefix"] alias_suffix = data["alias_suffix"] # make sure alias_prefix is not empty alias_prefix = alias_prefix.strip() alias_prefix = convert_to_id(alias_prefix) if not alias_prefix: # should be checked on frontend LOG.d("user %s submits empty alias prefix %s", user, alias_prefix) return jsonify(error="alias prefix cannot be empty"), 400 # make sure alias_suffix is either [email protected] or @my-domain.com alias_suffix = alias_suffix.strip() if alias_suffix.startswith("@"): custom_domain = alias_suffix[1:] if custom_domain not in user_custom_domains: LOG.d("user %s submits wrong custom domain %s ", user, custom_domain) return jsonify(error="error"), 400 else: if not alias_suffix.startswith("."): LOG.d("user %s submits wrong alias suffix %s", user, alias_suffix) return jsonify(error="error"), 400 if not alias_suffix.endswith(EMAIL_DOMAIN): LOG.d("user %s submits wrong alias suffix %s", user, alias_suffix) return jsonify(error="error"), 400 random_letters = alias_suffix[1 : alias_suffix.find("@")] if len(random_letters) < 5: LOG.d("user %s submits wrong alias suffix %s", user, alias_suffix) return jsonify(error="error"), 400 full_alias = alias_prefix + alias_suffix if GenEmail.get_by(email=full_alias): LOG.d("full alias already used %s", full_alias) return jsonify(error=f"alias {full_alias} already exists"), 409 gen_email = GenEmail.create(user_id=user.id, email=full_alias) db.session.commit() if hostname: AliasUsedOn.create(gen_email_id=gen_email.id, hostname=hostname) db.session.commit() return jsonify(alias=full_alias), 201
def authorize(): """ Redirected from client when user clicks on "Login with Server". This is a GET request with the following field in url - client_id - (optional) state - response_type: must be code """ oauth_client_id = request.args.get("client_id") state = request.args.get("state") scope = request.args.get("scope") redirect_uri = request.args.get("redirect_uri") response_mode = request.args.get("response_mode") nonce = request.args.get("nonce") try: response_types: [ResponseType] = get_response_types(request) except ValueError: return ( "response_type must be code, token, id_token or certain combination of these." " Please see /.well-known/openid-configuration to see what response_type are supported ", 400, ) if set(response_types) not in SUPPORTED_OPENID_FLOWS: return ( f"SimpleLogin only support the following OIDC flows: {SUPPORTED_OPENID_FLOWS_STR}", 400, ) if not redirect_uri: LOG.d("no redirect uri") return "redirect_uri must be set", 400 client = Client.get_by(oauth_client_id=oauth_client_id) if not client: final_redirect_uri = ( f"{redirect_uri}?error=invalid_client_id&client_id={oauth_client_id}" ) return redirect(final_redirect_uri) # check if redirect_uri is valid # allow localhost by default hostname, scheme = get_host_name_and_scheme(redirect_uri) if hostname != "localhost" and hostname != "127.0.0.1": # support custom scheme for mobile app if scheme == "http": final_redirect_uri = f"{redirect_uri}?error=http_not_allowed" return redirect(final_redirect_uri) if not RedirectUri.get_by(client_id=client.id, uri=redirect_uri): final_redirect_uri = f"{redirect_uri}?error=unknown_redirect_uri" return redirect(final_redirect_uri) # redirect from client website if request.method == "GET": if current_user.is_authenticated: suggested_email, other_emails, email_suffix = None, [], None suggested_name, other_names = None, [] # user has already allowed this client client_user: ClientUser = ClientUser.get_by( client_id=client.id, user_id=current_user.id) user_info = {} if client_user: LOG.debug("user %s has already allowed client %s", current_user, client) user_info = client_user.get_user_info() else: suggested_email, other_emails = current_user.suggested_emails( client.name) suggested_name, other_names = current_user.suggested_names() 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( "oauth/authorize.html", Scope=Scope, EMAIL_DOMAIN=EMAIL_DOMAIN, **locals(), ) else: # after user logs in, redirect user back to this page return render_template( "oauth/authorize_nonlogin_user.html", client=client, next=request.url, Scope=Scope, ) else: # POST - user allows or denies if request.form.get("button") == "deny": LOG.debug("User %s denies Client %s", current_user, client) final_redirect_uri = f"{redirect_uri}?error=deny&state={state}" return redirect(final_redirect_uri) LOG.debug("User %s allows Client %s", current_user, client) client_user = ClientUser.get_by(client_id=client.id, user_id=current_user.id) # user has already allowed this client, user cannot change information if client_user: LOG.d("user %s has already allowed client %s", current_user, client) else: alias_prefix = request.form.get("prefix") alias_suffix = request.form.get("suffix") gen_email = None # 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(): raise Exception( f"User {current_user} cannot create custom email") user_custom_domains = [ cd.domain for cd in current_user.verified_custom_domains() ] from app.dashboard.views.custom_alias import verify_prefix_suffix 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, mailbox_id=current_user.default_mailbox_id, ) # 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) # User chooses one of the suggestions else: chosen_email = request.form.get("suggested-email") # todo: add some checks on chosen_email if chosen_email != current_user.email: gen_email = GenEmail.get_by(email=chosen_email) if not gen_email: gen_email = GenEmail.create( email=chosen_email, user_id=current_user.id, mailbox_id=current_user.default_mailbox_id, ) 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_id=client.id, user_id=current_user.id) if gen_email: client_user.gen_email_id = gen_email.id if custom_name: client_user.name = custom_name elif suggested_name != current_user.name: client_user.name = suggested_name if use_default_avatar: # use default avatar LOG.d("use default avatar for user %s client %s", current_user, client) client_user.default_avatar = True db.session.flush() LOG.d("create client-user for client %s, user %s", client, current_user) redirect_args = {} if state: redirect_args["state"] = state else: LOG.warning( "more security reason, state should be added. client %s", client) if scope: redirect_args["scope"] = scope auth_code = None if ResponseType.CODE in response_types: # Create authorization code auth_code = AuthorizationCode.create( client_id=client.id, user_id=current_user.id, code=random_string(), scope=scope, redirect_uri=redirect_uri, response_type=response_types_to_str(response_types), ) db.session.add(auth_code) redirect_args["code"] = auth_code.code oauth_token = None if ResponseType.TOKEN in response_types: # create access-token oauth_token = OauthToken.create( client_id=client.id, user_id=current_user.id, scope=scope, redirect_uri=redirect_uri, access_token=generate_access_token(), response_type=response_types_to_str(response_types), ) db.session.add(oauth_token) redirect_args["access_token"] = oauth_token.access_token if ResponseType.ID_TOKEN in response_types: redirect_args["id_token"] = make_id_token( client_user, nonce, oauth_token.access_token if oauth_token else None, auth_code.code if auth_code else None, ) db.session.commit() # should all params appended the url using fragment (#) or query fragment = False if response_mode and response_mode == "fragment": fragment = True # if response_types contain "token" => implicit flow => should use fragment # except if client sets explicitly response_mode if not response_mode: if ResponseType.TOKEN in response_types: fragment = True # construct redirect_uri with redirect_args return redirect(construct_url(redirect_uri, redirect_args, fragment))
def authorize(): """ Redirected from client when user clicks on "Login with Server". This is a GET request with the following field in url - client_id - (optional) state - response_type: must be code """ oauth_client_id = request.args.get("client_id") state = request.args.get("state") scope = request.args.get("scope") redirect_uri = request.args.get("redirect_uri") response_mode = request.args.get("response_mode") nonce = request.args.get("nonce") try: response_types: [ResponseType] = get_response_types(request) except ValueError: return ( "response_type must be code, token, id_token or certain combination of these." " Please see /.well-known/openid-configuration to see what response_type are supported ", 400, ) if set(response_types) not in SUPPORTED_OPENID_FLOWS: return ( f"SimpleLogin only support the following OIDC flows: {SUPPORTED_OPENID_FLOWS_STR}", 400, ) if not redirect_uri: LOG.d("no redirect uri") return "redirect_uri must be set", 400 client = Client.get_by(oauth_client_id=oauth_client_id) if not client: final_redirect_uri = ( f"{redirect_uri}?error=invalid_client_id&client_id={oauth_client_id}" ) return redirect(final_redirect_uri) # check if redirect_uri is valid # allow localhost by default hostname, scheme = get_host_name_and_scheme(redirect_uri) if hostname != "localhost" and hostname != "127.0.0.1": # support custom scheme for mobile app if scheme == "http": final_redirect_uri = f"{redirect_uri}?error=http_not_allowed" return redirect(final_redirect_uri) if not RedirectUri.get_by(client_id=client.id, uri=redirect_uri): final_redirect_uri = f"{redirect_uri}?error=unknown_redirect_uri" return redirect(final_redirect_uri) # redirect from client website if request.method == "GET": if current_user.is_authenticated: suggested_email, other_emails, email_suffix = None, [], None suggested_name, other_names = None, [] # user has already allowed this client client_user: ClientUser = ClientUser.get_by( client_id=client.id, user_id=current_user.id ) user_info = {} if client_user: LOG.debug("user %s has already allowed client %s", current_user, client) user_info = client_user.get_user_info() else: suggested_email, other_emails = current_user.suggested_emails( client.name ) suggested_name, other_names = current_user.suggested_names() email_suffix = random_word() return render_template( "oauth/authorize.html", client=client, user_info=user_info, client_user=client_user, 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, ) else: # after user logs in, redirect user back to this page return render_template( "oauth/authorize_nonlogin_user.html", client=client, next=request.url, Scope=Scope, ) else: # user allows or denies if request.form.get("button") == "deny": LOG.debug("User %s denies Client %s", current_user, client) final_redirect_uri = f"{redirect_uri}?error=deny&state={state}" return redirect(final_redirect_uri) LOG.debug("User %s allows Client %s", current_user, client) client_user = ClientUser.get_by(client_id=client.id, user_id=current_user.id) # user has already allowed this client, user cannot change information if client_user: LOG.d("user %s has already allowed client %s", current_user, client) else: email_suffix = request.form.get("email-suffix") custom_email_prefix = request.form.get("custom-email-prefix") 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 if custom_email_prefix: # check if user can generate custom email if not current_user.can_create_new_alias(): raise Exception(f"User {current_user} cannot create custom email") email = f"{convert_to_id(custom_email_prefix)}.{email_suffix}@{EMAIL_DOMAIN}" LOG.d("create custom email alias %s for user %s", email, current_user) if GenEmail.get_by(email=email) or DeletedAlias.get_by(email=email): LOG.error("email %s already used, very rare!", email) flash(f"alias {email} already used", "error") return redirect(request.url) gen_email = GenEmail.create(email=email, user_id=current_user.id) db.session.flush() else: # user picks an email from suggestion if chosen_email != current_user.email: gen_email = GenEmail.get_by(email=chosen_email) if not gen_email: gen_email = GenEmail.create( email=chosen_email, user_id=current_user.id ) db.session.flush() client_user = ClientUser.create( client_id=client.id, user_id=current_user.id ) if gen_email: client_user.gen_email_id = gen_email.id if custom_name: LOG.d( "use custom name %s for user %s client %s", custom_name, current_user, client, ) client_user.name = custom_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 if use_default_avatar: # use default avatar LOG.d("use default avatar for user %s client %s", current_user, client) client_user.default_avatar = True db.session.flush() LOG.d("create client-user for client %s, user %s", client, current_user) redirect_args = {} if state: redirect_args["state"] = state else: LOG.warning( "more security reason, state should be added. client %s", client ) if scope: redirect_args["scope"] = scope auth_code = None if ResponseType.CODE in response_types: # Create authorization code auth_code = AuthorizationCode.create( client_id=client.id, user_id=current_user.id, code=random_string(), scope=scope, redirect_uri=redirect_uri, response_type=response_types_to_str(response_types), ) db.session.add(auth_code) redirect_args["code"] = auth_code.code oauth_token = None if ResponseType.TOKEN in response_types: # create access-token oauth_token = OauthToken.create( client_id=client.id, user_id=current_user.id, scope=scope, redirect_uri=redirect_uri, access_token=generate_access_token(), response_type=response_types_to_str(response_types), ) db.session.add(oauth_token) redirect_args["access_token"] = oauth_token.access_token if ResponseType.ID_TOKEN in response_types: redirect_args["id_token"] = make_id_token( client_user, nonce, oauth_token.access_token if oauth_token else None, auth_code.code if auth_code else None, ) db.session.commit() # should all params appended the url using fragment (#) or query fragment = False if response_mode and response_mode == "fragment": fragment = True # if response_types contain "token" => implicit flow => should use fragment # except if client sets explicitly response_mode if not response_mode: if ResponseType.TOKEN in response_types: fragment = True # construct redirect_uri with redirect_args return redirect(construct_url(redirect_uri, redirect_args, fragment))