Add authentication from upstream providers (#107)
All checks were successful
continuous-integration/drone/push Build is passing

Let BasicOIDC delegate authentication to upstream providers (Google, GitHub, GitLab, Keycloak...)

Reviewed-on: #107
This commit is contained in:
2023-04-27 10:10:28 +00:00
parent 4f7c56a4b8
commit 9b18b787a9
39 changed files with 1740 additions and 189 deletions

View File

@ -30,8 +30,6 @@
font-size: 3.5rem;
}
}
</style>
@ -45,7 +43,7 @@
<main class="form-signin">
<h1 class="h3 mb-3 fw-normal">{{ _p.page_title }}</h1>
<h1 class="h3 mb-3 fw-normal" style="margin-bottom: 2rem !important;">{{ _p.page_title }}</h1>
{% if let Some(danger) = _p.danger %}
<div class="alert alert-danger" role="alert">

View File

@ -1,5 +1,23 @@
{% extends "base_login_page.html" %}
{% block content %}
<style>
#providers {
margin-top: 40px;
}
.provider-button {
width: 100%;
display: flex;
margin-top: 10px;
}
.provider-button img {
margin-right: 1em;
width: 1em;
}
</style>
<form action="/login?redirect={{ _p.redirect_uri.get_encoded() }}" method="post">
<div>
<div class="form-floating">
@ -19,4 +37,18 @@
</form>
<!-- Upstream providers -->
{% if !providers.is_empty() %}
<div id="providers">
{% for prov in providers %}
<a class="btn btn-secondary btn-lg provider-button" href="{{ prov.login_url(_p.redirect_uri) }}">
<img src="{{ prov.logo_url() }}" alt="Provider icon"/>
<div style="text-align: left;">
Login using {{ prov.name }} <br/>
</div>
</a>
{% endfor %}
</div>
{% endif %}
{% endblock content %}

View File

@ -0,0 +1,13 @@
{% extends "base_login_page.html" %}
{% block content %}
<div class="alert alert-danger" style="margin-bottom: 10px;">
<strong>Authentication failed!</strong>
<p style="margin-top: 10px; text-align: justify;">{{ message }}</p>
</div>
<a href="/login?redirect={{ _p.redirect_uri.get_encoded() }}">Go back to login</a>
{% endblock content %}

View File

@ -5,27 +5,27 @@
<tbody>
<tr>
<th scope="row">User ID</th>
<td>{{ u.uid.0 }}</td>
<td>{{ _p.user.uid.0 }}</td>
</tr>
<tr>
<th scope="row">First name</th>
<td>{{ u.first_name }}</td>
<td>{{ _p.user.first_name }}</td>
</tr>
<tr>
<th scope="row">Last name</th>
<td>{{ u.last_name }}</td>
<td>{{ _p.user.last_name }}</td>
</tr>
<tr>
<th scope="row">Username</th>
<td>{{ u.username }}</td>
<td>{{ _p.user.username }}</td>
</tr>
<tr>
<th scope="row">Email</th>
<td>{{ u.email }}</td>
<td>{{ _p.user.email }}</td>
</tr>
<tr>
<th scope="row">Account type</th>
<td>{% if u.admin %}Admin{% else %}Regular user{% endif %}</td>
<td>{% if _p.user.admin %}Admin{% else %}Regular user{% endif %}</td>
</tr>
</tbody>
</table>

View File

@ -14,8 +14,8 @@
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-dark text-decoration-none">
<span class="fs-4">{{ _p.app_name }}</span>
</a>
{% if _p.is_admin %}
<span>Version {{ _p.version }}</span>
{% if _p.user.admin %}
<span>Version {{ _p.version }}</span>
{% endif %}
<hr>
<ul class="nav nav-pills flex-column mb-auto">
@ -24,24 +24,31 @@
Account details
</a>
</li>
{% if _p.user.allow_local_login %}
<li>
<a href="/settings/change_password" class="nav-link link-dark">
Change password
</a>
</li>
{% endif %}
<li>
<a href="/settings/two_factors" class="nav-link link-dark">
Two-factor authentication
</a>
</li>
{% if _p.is_admin %}
{% if _p.user.admin %}
<hr/>
<li>
<a href="/admin/clients" class="nav-link link-dark">
Clients
</a>
</li>
<li>
<a href="/admin/providers" class="nav-link link-dark">
Providers
</a>
</li>
<li>
<a href="/admin/users" class="nav-link link-dark">
Users
@ -54,7 +61,7 @@
<a href="#" class="d-flex align-items-center link-dark text-decoration-none dropdown-toggle" id="dropdownUser"
data-bs-toggle="dropdown" aria-expanded="false">
<img src="/assets/img/account.png" alt="" width="32" height="32" class="rounded-circle me-2">
<strong>{{ _p.user_name }}</strong>
<strong>{{ _p.user.username }}</strong>
</a>
<ul class="dropdown-menu text-small shadow" aria-labelledby="dropdownUser">
<li><a class="dropdown-item" href="/logout">Sign out</a></li>
@ -83,6 +90,7 @@
if(el.href === location.href) el.classList.add("active");
else el.classList.remove("active")
})
</script>
{% if _p.ip_location_api.is_some() %}
<script>const IP_LOCATION_API = "{{ _p.ip_location_api.unwrap() }}"</script>

View File

@ -112,28 +112,61 @@
</div>
<ul>
{% for e in u.get_formatted_2fa_successful_logins() %}
{% if e.can_bypass_2fa %}<li style="font-weight: bold;">{{ e.ip }} - {{ e.fmt_time() }} - BYPASS 2FA</li>
{% else %}<li>{{ e.ip }} - {{ e.fmt_time() }}</li>{% endif %}
{% endfor %}
{% for e in u.get_formatted_2fa_successful_logins() %}
{% if e.can_bypass_2fa %}
<li style="font-weight: bold;">{{ e.ip }} - {{ e.fmt_time() }} - BYPASS 2FA</li>
{% else %}
<li>{{ e.ip }} - {{ e.fmt_time() }}</li>
{% endif %}
{% endfor %}
</ul>
</fieldset>
{% endif %}
<!-- Authorized authentication sources -->
<fieldset class="form-group">
<legend class="mt-4">Authorized authentication sources</legend>
<!-- Local login -->
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_local_login" id="allow_local_login"
{% if u.allow_local_login %} checked="" {% endif %}>
<label class="form-check-label" for="allow_local_login">
Allow local login
</label>
</div>
<!-- Upstream providers -->
<input type="hidden" name="authorized_sources" id="authorized_sources"/>
{% for prov in providers %}
<div class="form-check">
<input class="form-check-input authorized_provider" type="checkbox" name="prov-{{ prov.id.0 }}"
id="prov-{{ prov.id.0 }}"
data-id="{{ prov.id.0 }}"
{% if u.can_login_from_provider(prov) %} checked="" {% endif %}>
<label class="form-check-label" for="prov-{{ prov.id.0 }}">
Allow login from {{ prov.name }}
</label>
</div>
{% endfor %}
</fieldset>
<!-- Granted clients -->
<fieldset class="form-group">
<legend class="mt-4">Granted clients</legend>
<div class="form-check">
<label class="form-check-label">
<input type="radio" class="form-check-input" name="grant_type"
value="all_clients" {% if u.granted_clients() == GrantedClients::AllClients %} checked="" {% endif %}>
value="all_clients" {% if u.granted_clients()== GrantedClients::AllClients %} checked="" {% endif
%}>
Grant all clients
</label>
</div>
<div class="form-check">
<label class="form-check-label">
<input type="radio" class="form-check-input" name="grant_type"
value="custom_clients" {% if matches!(self.u.granted_clients(), GrantedClients::SomeClients(_)) %} checked="checked" {% endif %}>
value="custom_clients" {% if matches!(self.u.granted_clients(), GrantedClients::SomeClients(_))
%} checked="checked" {% endif %}>
Manually specify allowed clients
</label>
</div>
@ -155,7 +188,8 @@
<div class="form-check">
<label class="form-check-label">
<input type="radio" class="form-check-input" name="grant_type"
value="no_client" {% if u.granted_clients() == GrantedClients::NoClient %} checked="checked" {% endif %}>
value="no_client" {% if u.granted_clients()== GrantedClients::NoClient %} checked="checked" {%
endif %}>
Do not grant any client
</label>
</div>
@ -215,6 +249,13 @@
form.addEventListener("submit", (ev) => {
ev.preventDefault();
const authorized_sources = [...document.querySelectorAll(".authorized_provider")]
.filter(e => e.checked)
.map(e => e.getAttribute("data-id")).join(",")
document.querySelector("input[name=authorized_sources]").value = authorized_sources;
const authorized_clients = [...document.querySelectorAll(".authorize_client_checkbox")]
.filter(e => e.checked)
.map(e => e.getAttribute("data-id")).join(",")
@ -231,6 +272,9 @@
form.submit();
});
</script>
{% endblock content %}

View File

@ -0,0 +1,39 @@
{% extends "base_settings_page.html" %}
{% block content %}
<style>
#providers td {
vertical-align: middle;
}
</style>
<table id="providers" class="table table-hover" style="max-width: 800px;" aria-describedby="OpenID providers list">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Configuration URL</th>
<th scope="col">Client ID</th>
</tr>
</thead>
<tbody>
{% for c in providers %}
<tr>
<td>
<img src="{{ c.logo_url() }}" alt="{{ c.name }} logo" width="30px"/>
</td>
<td>{{ c.id.0 }}</td>
<td>{{ c.name }}</td>
<td><a href="{{ c.configuration_url }}" target="_blank" rel="noreferrer">
{{ c.configuration_url }}
</a></td>
<td>{{ c.client_id }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>Redirect URL for new clients: {{ redirect_url }}</p>
{% endblock content %}