Introduce support for 3rd-party auth (OAuth2 & LDAP)
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
0f8c154d4b
commit
7302ecbc23
@ -118,6 +118,30 @@ config :guardian, Guardian.DB,
|
||||
|
||||
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
|
||||
|
||||
config :mobilizon,
|
||||
Mobilizon.Service.Auth.Authenticator,
|
||||
Mobilizon.Service.Auth.MobilizonAuthenticator
|
||||
|
||||
config :ueberauth,
|
||||
Ueberauth,
|
||||
providers: []
|
||||
|
||||
config :mobilizon, :auth, oauth_consumer_strategies: []
|
||||
|
||||
config :mobilizon, :ldap,
|
||||
enabled: System.get_env("LDAP_ENABLED") == "true",
|
||||
host: System.get_env("LDAP_HOST") || "localhost",
|
||||
port: String.to_integer(System.get_env("LDAP_PORT") || "389"),
|
||||
ssl: System.get_env("LDAP_SSL") == "true",
|
||||
sslopts: [],
|
||||
tls: System.get_env("LDAP_TLS") == "true",
|
||||
tlsopts: [],
|
||||
base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
|
||||
uid: System.get_env("LDAP_UID") || "cn",
|
||||
require_bind_for_search: !(System.get_env("LDAP_REQUIRE_BIND_FOR_SEARCH") == "false"),
|
||||
bind_uid: System.get_env("LDAP_BIND_UID"),
|
||||
bind_password: System.get_env("LDAP_BIND_PASSWORD")
|
||||
|
||||
config :geolix,
|
||||
databases: [
|
||||
%{
|
||||
|
26
js/src/components/User/AuthProvider.vue
Normal file
26
js/src/components/User/AuthProvider.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<a
|
||||
class="button is-light"
|
||||
v-if="Object.keys(SELECTED_PROVIDERS).includes(oauthProvider.id)"
|
||||
:href="`/auth/${oauthProvider.id}`"
|
||||
>
|
||||
<b-icon :icon="oauthProvider.id" />
|
||||
<span>{{ SELECTED_PROVIDERS[oauthProvider.id] }}</span></a
|
||||
>
|
||||
<a class="button is-light" :href="`/auth/${oauthProvider.id}`" v-else>
|
||||
<b-icon icon="lock" />
|
||||
<span>{{ oauthProvider.label }}</span>
|
||||
</a>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import { IOAuthProvider } from "../../types/config.model";
|
||||
import { SELECTED_PROVIDERS } from "../../utils/auth";
|
||||
|
||||
@Component
|
||||
export default class AuthProvider extends Vue {
|
||||
@Prop({ required: true, type: Object }) oauthProvider!: IOAuthProvider;
|
||||
|
||||
SELECTED_PROVIDERS = SELECTED_PROVIDERS;
|
||||
}
|
||||
</script>
|
26
js/src/components/User/AuthProviders.vue
Normal file
26
js/src/components/User/AuthProviders.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<b>{{ $t("Sign in with") }}</b>
|
||||
<div class="buttons">
|
||||
<auth-provider
|
||||
v-for="provider in oauthProviders"
|
||||
:oauthProvider="provider"
|
||||
:key="provider.id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||
import { IOAuthProvider } from "../../types/config.model";
|
||||
import AuthProvider from "./AuthProvider.vue";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
AuthProvider,
|
||||
},
|
||||
})
|
||||
export default class AuthProviders extends Vue {
|
||||
@Prop({ required: true, type: Array }) oauthProviders!: IOAuthProvider[];
|
||||
}
|
||||
</script>
|
@ -62,6 +62,13 @@ export const CONFIG = gql`
|
||||
features {
|
||||
groups
|
||||
}
|
||||
auth {
|
||||
ldap
|
||||
oauthProviders {
|
||||
id
|
||||
label
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -35,6 +35,15 @@ export const LOGGED_USER = gql`
|
||||
loggedUser {
|
||||
id
|
||||
email
|
||||
defaultActor {
|
||||
id
|
||||
preferredUsername
|
||||
name
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
}
|
||||
provider
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -64,7 +73,7 @@ export const VALIDATE_EMAIL = gql`
|
||||
`;
|
||||
|
||||
export const DELETE_ACCOUNT = gql`
|
||||
mutation DeleteAccount($password: String, $userId: ID!) {
|
||||
mutation DeleteAccount($password: String, $userId: ID) {
|
||||
deleteAccount(password: $password, userId: $userId) {
|
||||
id
|
||||
}
|
||||
|
@ -703,5 +703,10 @@
|
||||
"New discussion": "New discussion",
|
||||
"Create a discussion": "Create a discussion",
|
||||
"Create the discussion": "Create the discussion",
|
||||
"View all discussions": "View all discussions"
|
||||
"View all discussions": "View all discussions",
|
||||
"Sign in with": "Sign in with",
|
||||
"Your email address was automatically set based on your {provider} account.": "Your email address was automatically set based on your {provider} account.",
|
||||
"You can't change your password because you are registered through {provider}.": "You can't change your password because you are registered through {provider}.",
|
||||
"Error while login with {provider}. Retry or login another way.": "Error while login with {provider}. Retry or login another way.",
|
||||
"Error while login with {provider}. This login provider doesn't exist.": "Error while login with {provider}. This login provider doesn't exist."
|
||||
}
|
||||
|
@ -703,5 +703,10 @@
|
||||
"{number} participations": "Aucune participation|Une participation|{number} participations",
|
||||
"{profile} (by default)": "{profile} (par défault)",
|
||||
"{title} ({count} todos)": "{title} ({count} todos)",
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap"
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
|
||||
"Sign in with": "Se connecter avec",
|
||||
"Your email address was automatically set based on your {provider} account.": "Votre adresse email a été définie automatiquement en se basant sur votre compte {provider}.",
|
||||
"You can't change your password because you are registered through {provider}.": "Vous ne pouvez pas changer votre mot de passe car vous vous êtes enregistré via {provider}.",
|
||||
"Error while login with {provider}. Retry or login another way.": "Erreur lors de la connexion avec {provider}. Réessayez ou bien connectez vous autrement.",
|
||||
"Error while login with {provider}. This login provider doesn't exist.": "Erreur lors de la connexion avec {provider}. Cette méthode de connexion n'existe pas."
|
||||
}
|
||||
|
@ -112,6 +112,11 @@ const router = new Router({
|
||||
component: () => import(/* webpackChunkName: "cookies" */ "@/views/Interact.vue"),
|
||||
meta: { requiredAuth: false },
|
||||
},
|
||||
{
|
||||
path: "/auth/:provider/callback",
|
||||
name: "auth-callback",
|
||||
component: () => import("@/views/User/ProviderValidation.vue"),
|
||||
},
|
||||
{
|
||||
path: "/404",
|
||||
name: RouteName.PAGE_NOT_FOUND,
|
||||
|
@ -74,4 +74,13 @@ export interface IConfig {
|
||||
};
|
||||
federating: boolean;
|
||||
version: string;
|
||||
auth: {
|
||||
ldap: boolean;
|
||||
oauthProviders: IOAuthProvider[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface IOAuthProvider {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
@ -9,15 +9,11 @@ export enum ICurrentUserRole {
|
||||
}
|
||||
|
||||
export interface ICurrentUser {
|
||||
id: number;
|
||||
id: string;
|
||||
email: string;
|
||||
isLoggedIn: boolean;
|
||||
role: ICurrentUserRole;
|
||||
participations: Paginate<IParticipant>;
|
||||
defaultActor: IPerson;
|
||||
drafts: IEvent[];
|
||||
settings: IUserSettings;
|
||||
locale: string;
|
||||
defaultActor?: IPerson;
|
||||
}
|
||||
|
||||
export interface IUser extends ICurrentUser {
|
||||
@ -25,6 +21,22 @@ export interface IUser extends ICurrentUser {
|
||||
confirmationSendAt: Date;
|
||||
actors: IPerson[];
|
||||
disabled: boolean;
|
||||
participations: Paginate<IParticipant>;
|
||||
drafts: IEvent[];
|
||||
settings: IUserSettings;
|
||||
locale: string;
|
||||
provider?: string;
|
||||
}
|
||||
|
||||
export enum IAuthProvider {
|
||||
LDAP = "ldap",
|
||||
GOOGLE = "google",
|
||||
DISCORD = "discord",
|
||||
GITHUB = "github",
|
||||
KEYCLOAK = "keycloak",
|
||||
FACEBOOK = "facebook",
|
||||
GITLAB = "gitlab",
|
||||
TWITTER = "twitter",
|
||||
}
|
||||
|
||||
export enum INotificationPendingParticipationEnum {
|
||||
|
@ -6,4 +6,6 @@ export enum LoginError {
|
||||
USER_NOT_CONFIRMED = "User account not confirmed",
|
||||
USER_DOES_NOT_EXIST = "No user with this email was found",
|
||||
USER_EMAIL_PASSWORD_INVALID = "Impossible to authenticate, either your email or password are invalid.",
|
||||
LOGIN_PROVIDER_ERROR = "Error with Login Provider",
|
||||
LOGIN_PROVIDER_NOT_FOUND = "Login Provider not found",
|
||||
}
|
||||
|
@ -94,3 +94,14 @@ export async function logout(apollo: ApolloClient<NormalizedCacheObject>) {
|
||||
|
||||
deleteUserData();
|
||||
}
|
||||
|
||||
export const SELECTED_PROVIDERS: { [key: string]: string } = {
|
||||
twitter: "Twitter",
|
||||
discord: "Discord",
|
||||
facebook: "Facebook",
|
||||
github: "Github",
|
||||
gitlab: "Gitlab",
|
||||
google: "Google",
|
||||
keycloak: "Keycloak",
|
||||
ldap: "LDAP",
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="loggedUser">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
@ -24,6 +24,13 @@
|
||||
>
|
||||
<b slot="email">{{ loggedUser.email }}</b>
|
||||
</i18n>
|
||||
<b-message v-if="!canChangeEmail" type="is-warning" :closable="false">
|
||||
{{
|
||||
$t("Your email address was automatically set based on your {provider} account.", {
|
||||
provider: providerName(loggedUser.provider),
|
||||
})
|
||||
}}
|
||||
</b-message>
|
||||
<b-notification
|
||||
type="is-danger"
|
||||
has-icon
|
||||
@ -33,7 +40,7 @@
|
||||
v-for="error in changeEmailErrors"
|
||||
>{{ error }}</b-notification
|
||||
>
|
||||
<form @submit.prevent="resetEmailAction" ref="emailForm" class="form">
|
||||
<form @submit.prevent="resetEmailAction" ref="emailForm" class="form" v-if="canChangeEmail">
|
||||
<b-field :label="$t('New email')">
|
||||
<b-input aria-required="true" required type="email" v-model="newEmail" />
|
||||
</b-field>
|
||||
@ -58,6 +65,13 @@
|
||||
<div class="setting-title">
|
||||
<h2>{{ $t("Password") }}</h2>
|
||||
</div>
|
||||
<b-message v-if="!canChangePassword" type="is-warning" :closable="false">
|
||||
{{
|
||||
$t("You can't change your password because you are registered through {provider}.", {
|
||||
provider: providerName(loggedUser.provider),
|
||||
})
|
||||
}}
|
||||
</b-message>
|
||||
<b-notification
|
||||
type="is-danger"
|
||||
has-icon
|
||||
@ -67,7 +81,12 @@
|
||||
v-for="error in changePasswordErrors"
|
||||
>{{ error }}</b-notification
|
||||
>
|
||||
<form @submit.prevent="resetPasswordAction" ref="passwordForm" class="form">
|
||||
<form
|
||||
@submit.prevent="resetPasswordAction"
|
||||
ref="passwordForm"
|
||||
class="form"
|
||||
v-if="canChangePassword"
|
||||
>
|
||||
<b-field :label="$t('Old password')">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
@ -124,11 +143,11 @@
|
||||
<br />
|
||||
<b>{{ $t("There will be no way to recover your data.") }}</b>
|
||||
</p>
|
||||
<p class="content">
|
||||
<p class="content" v-if="hasUserGotAPassword">
|
||||
{{ $t("Please enter your password to confirm this action.") }}
|
||||
</p>
|
||||
<form @submit.prevent="deleteAccount">
|
||||
<b-field>
|
||||
<b-field v-if="hasUserGotAPassword">
|
||||
<b-input
|
||||
type="password"
|
||||
v-model="passwordForAccountDeletion"
|
||||
@ -160,8 +179,8 @@
|
||||
import { Component, Vue, Ref } from "vue-property-decorator";
|
||||
import { CHANGE_EMAIL, CHANGE_PASSWORD, DELETE_ACCOUNT, LOGGED_USER } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { ICurrentUser } from "../../types/current-user.model";
|
||||
import { logout } from "../../utils/auth";
|
||||
import { IUser, IAuthProvider } from "../../types/current-user.model";
|
||||
import { logout, SELECTED_PROVIDERS } from "../../utils/auth";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@ -171,7 +190,7 @@ import { logout } from "../../utils/auth";
|
||||
export default class AccountSettings extends Vue {
|
||||
@Ref("passwordForm") readonly passwordForm!: HTMLElement;
|
||||
|
||||
loggedUser!: ICurrentUser;
|
||||
loggedUser!: IUser;
|
||||
|
||||
passwordForEmailChange = "";
|
||||
|
||||
@ -243,7 +262,7 @@ export default class AccountSettings extends Vue {
|
||||
await this.$apollo.mutate({
|
||||
mutation: DELETE_ACCOUNT,
|
||||
variables: {
|
||||
password: this.passwordForAccountDeletion,
|
||||
password: this.hasUserGotAPassword ? this.passwordForAccountDeletion : null,
|
||||
},
|
||||
});
|
||||
await logout(this.$apollo.provider.defaultClient);
|
||||
@ -260,6 +279,28 @@ export default class AccountSettings extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
get canChangePassword() {
|
||||
return !this.loggedUser.provider;
|
||||
}
|
||||
|
||||
get canChangeEmail() {
|
||||
return !this.loggedUser.provider;
|
||||
}
|
||||
|
||||
providerName(id: string) {
|
||||
if (SELECTED_PROVIDERS[id]) {
|
||||
return SELECTED_PROVIDERS[id];
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
get hasUserGotAPassword(): boolean {
|
||||
return (
|
||||
this.loggedUser &&
|
||||
(this.loggedUser.provider == null || this.loggedUser.provider == IAuthProvider.LDAP)
|
||||
);
|
||||
}
|
||||
|
||||
private handleErrors(type: string, err: any) {
|
||||
console.error(err);
|
||||
|
||||
|
@ -95,10 +95,7 @@
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import { USER_SETTINGS, SET_USER_SETTINGS } from "../../graphql/user";
|
||||
import {
|
||||
ICurrentUser,
|
||||
INotificationPendingParticipationEnum,
|
||||
} from "../../types/current-user.model";
|
||||
import { IUser, INotificationPendingParticipationEnum } from "../../types/current-user.model";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
@ -107,7 +104,7 @@ import RouteName from "../../router/name";
|
||||
},
|
||||
})
|
||||
export default class Notifications extends Vue {
|
||||
loggedUser!: ICurrentUser;
|
||||
loggedUser!: IUser;
|
||||
|
||||
notificationOnDay = true;
|
||||
|
||||
|
@ -52,7 +52,7 @@ import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import { TIMEZONES } from "../../graphql/config";
|
||||
import { USER_SETTINGS, SET_USER_SETTINGS, UPDATE_USER_LOCALE } from "../../graphql/user";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import { ICurrentUser } from "../../types/current-user.model";
|
||||
import { IUser } from "../../types/current-user.model";
|
||||
import langs from "../../i18n/langs.json";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@ -65,7 +65,7 @@ import RouteName from "../../router/name";
|
||||
export default class Preferences extends Vue {
|
||||
config!: IConfig;
|
||||
|
||||
loggedUser!: ICurrentUser;
|
||||
loggedUser!: IUser;
|
||||
|
||||
selectedTimezone: string | null = null;
|
||||
|
||||
@ -74,7 +74,7 @@ export default class Preferences extends Vue {
|
||||
RouteName = RouteName;
|
||||
|
||||
@Watch("loggedUser")
|
||||
setSavedTimezone(loggedUser: ICurrentUser) {
|
||||
setSavedTimezone(loggedUser: IUser) {
|
||||
if (loggedUser && loggedUser.settings.timezone) {
|
||||
this.selectedTimezone = loggedUser.settings.timezone;
|
||||
} else {
|
||||
|
@ -10,6 +10,26 @@
|
||||
:aria-close-label="$t('Close')"
|
||||
>{{ $t("You need to login.") }}</b-message
|
||||
>
|
||||
<b-message
|
||||
v-else-if="errorCode === LoginError.LOGIN_PROVIDER_ERROR"
|
||||
type="is-danger"
|
||||
:aria-close-label="$t('Close')"
|
||||
>{{
|
||||
$t("Error while login with {provider}. Retry or login another way.", {
|
||||
provider: $route.query.provider,
|
||||
})
|
||||
}}</b-message
|
||||
>
|
||||
<b-message
|
||||
v-else-if="errorCode === LoginError.LOGIN_PROVIDER_NOT_FOUND"
|
||||
type="is-danger"
|
||||
:aria-close-label="$t('Close')"
|
||||
>{{
|
||||
$t("Error while login with {provider}. This login provider doesn't exist.", {
|
||||
provider: $route.query.provider,
|
||||
})
|
||||
}}</b-message
|
||||
>
|
||||
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">
|
||||
<span v-if="error === LoginError.USER_NOT_CONFIRMED">
|
||||
<span>
|
||||
@ -60,6 +80,11 @@
|
||||
<p class="control has-text-centered">
|
||||
<button class="button is-primary is-large">{{ $t("Login") }}</button>
|
||||
</p>
|
||||
|
||||
<div class="control" v-if="config && config.auth.oauthProviders.length > 0">
|
||||
<auth-providers :oauthProviders="config.auth.oauthProviders" />
|
||||
</div>
|
||||
|
||||
<p class="control">
|
||||
<router-link
|
||||
class="button is-text"
|
||||
@ -103,6 +128,7 @@ import { LoginErrorCode, LoginError } from "../../types/login-error-code.model";
|
||||
import { ICurrentUser } from "../../types/current-user.model";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import AuthProviders from "../../components/User/AuthProviders.vue";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@ -113,6 +139,9 @@ import { IConfig } from "../../types/config.model";
|
||||
query: CURRENT_USER_CLIENT,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
AuthProviders,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
|
64
js/src/views/User/ProviderValidation.vue
Normal file
64
js/src/views/User/ProviderValidation.vue
Normal file
@ -0,0 +1,64 @@
|
||||
<template> </template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { VALIDATE_USER, UPDATE_CURRENT_USER_CLIENT, LOGGED_USER } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { saveUserData, changeIdentity } from "../../utils/auth";
|
||||
import { ILogin } from "../../types/login.model";
|
||||
import { ICurrentUserRole, ICurrentUser, IUser } from "../../types/current-user.model";
|
||||
import { IDENTITIES } from "../../graphql/actor";
|
||||
|
||||
@Component
|
||||
export default class ProviderValidate extends Vue {
|
||||
async mounted() {
|
||||
const accessToken = this.getValueFromMeta("auth-access-token");
|
||||
const refreshToken = this.getValueFromMeta("auth-refresh-token");
|
||||
const userId = this.getValueFromMeta("auth-user-id");
|
||||
const userEmail = this.getValueFromMeta("auth-user-email");
|
||||
const userRole = this.getValueFromMeta("auth-user-role") as ICurrentUserRole;
|
||||
const userActorId = this.getValueFromMeta("auth-user-actor-id");
|
||||
|
||||
if (!(userId && userEmail && userRole && accessToken && refreshToken)) {
|
||||
return this.$router.push("/");
|
||||
}
|
||||
const login = {
|
||||
user: { id: userId, email: userEmail, role: userRole, isLoggedIn: true },
|
||||
accessToken,
|
||||
refreshToken,
|
||||
};
|
||||
saveUserData(login);
|
||||
await this.$apollo.mutate({
|
||||
mutation: UPDATE_CURRENT_USER_CLIENT,
|
||||
variables: {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
isLoggedIn: true,
|
||||
role: ICurrentUserRole.USER,
|
||||
},
|
||||
});
|
||||
const { data } = await this.$apollo.query<{ loggedUser: IUser }>({
|
||||
query: LOGGED_USER,
|
||||
});
|
||||
const { loggedUser } = data;
|
||||
|
||||
if (loggedUser.defaultActor) {
|
||||
await changeIdentity(this.$apollo.provider.defaultClient, loggedUser.defaultActor);
|
||||
await this.$router.push({ name: RouteName.HOME });
|
||||
} else {
|
||||
// If the user didn't register any profile yet, let's create one for them
|
||||
await this.$router.push({
|
||||
name: RouteName.REGISTER_PROFILE,
|
||||
params: { email: loggedUser.email, userAlreadyActivated: "true" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getValueFromMeta(name: string) {
|
||||
const element = document.querySelector(`meta[name="${name}"]`);
|
||||
if (element && element.getAttribute("content")) {
|
||||
return element.getAttribute("content");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
</script>
|
@ -96,6 +96,7 @@
|
||||
{{ $t("Register") }}
|
||||
</b-button>
|
||||
</p>
|
||||
|
||||
<p class="control">
|
||||
<router-link
|
||||
class="button is-text"
|
||||
@ -113,6 +114,11 @@
|
||||
>{{ $t("Login") }}</router-link
|
||||
>
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
<div class="control" v-if="config && config.auth.oauthProviders.length > 0">
|
||||
<auth-providers :oauthProviders="config.auth.oauthProviders" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="errors.length > 0">
|
||||
@ -131,9 +137,10 @@ import RouteName from "../../router/name";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import Subtitle from "../../components/Utils/Subtitle.vue";
|
||||
import AuthProviders from "../../components/User/AuthProviders.vue";
|
||||
|
||||
@Component({
|
||||
components: { Subtitle },
|
||||
components: { Subtitle, AuthProviders },
|
||||
metaInfo() {
|
||||
return {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
|
@ -18,7 +18,7 @@
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { VALIDATE_USER, UPDATE_CURRENT_USER_CLIENT } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { saveUserData, changeIdentity } from "../../utils/auth";
|
||||
import { saveUserData, saveTokenData, changeIdentity } from "../../utils/auth";
|
||||
import { ILogin } from "../../types/login.model";
|
||||
import { ICurrentUserRole } from "../../types/current-user.model";
|
||||
|
||||
@ -45,6 +45,7 @@ export default class Validate extends Vue {
|
||||
|
||||
if (data) {
|
||||
saveUserData(data.validateUser);
|
||||
saveTokenData(data.validateUser);
|
||||
|
||||
const { user } = data.validateUser;
|
||||
|
||||
|
@ -124,7 +124,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
|
||||
},
|
||||
rules: Config.instance_rules(),
|
||||
version: Config.instance_version(),
|
||||
federating: Config.instance_federating()
|
||||
federating: Config.instance_federating(),
|
||||
auth: %{
|
||||
ldap: Config.ldap_enabled?(),
|
||||
oauth_providers: Config.oauth_consumer_strategies()
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
@ -202,10 +202,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||
"""
|
||||
def register_person(_parent, args, _resolution) do
|
||||
with {:ok, %User{} = user} <- Users.get_user_by_email(args.email),
|
||||
{:no_actor, nil} <- {:no_actor, Users.get_actor_for_user(user)},
|
||||
user_actor <- Users.get_actor_for_user(user),
|
||||
no_actor <- is_nil(user_actor),
|
||||
{:no_actor, true} <- {:no_actor, no_actor},
|
||||
args <- Map.put(args, :user_id, user.id),
|
||||
args <- save_attached_pictures(args),
|
||||
{:ok, %Actor{} = new_person} <- Actors.new_person(args) do
|
||||
{:ok, %Actor{} = new_person} <- Actors.new_person(args, true) do
|
||||
{:ok, new_person}
|
||||
else
|
||||
{:error, :user_not_found} ->
|
||||
|
@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Crypto
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Service.Auth.Authenticator
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users.{Setting, User}
|
||||
|
||||
@ -59,18 +60,16 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
Login an user. Returns a token and the user
|
||||
"""
|
||||
def login_user(_parent, %{email: email, password: password}, _resolution) do
|
||||
with {:ok, %User{confirmed_at: %DateTime{}} = user} <- Users.get_user_by_email(email),
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
|
||||
Users.authenticate(%{user: user, password: password}) do
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}}
|
||||
else
|
||||
{:ok, %User{confirmed_at: nil} = _user} ->
|
||||
{:error, "User account not confirmed"}
|
||||
case Authenticator.authenticate(email, password) do
|
||||
{:ok,
|
||||
%{access_token: _access_token, refresh_token: _refresh_token, user: _user} =
|
||||
user_and_tokens} ->
|
||||
{:ok, user_and_tokens}
|
||||
|
||||
{:error, :user_not_found} ->
|
||||
{:error, "No user with this email was found"}
|
||||
|
||||
{:error, :unauthorized} ->
|
||||
{:error, _error} ->
|
||||
{:error, "Impossible to authenticate, either your email or password are invalid."}
|
||||
end
|
||||
end
|
||||
@ -82,7 +81,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
with {:ok, user, _claims} <- Auth.Guardian.resource_from_token(refresh_token),
|
||||
{:ok, _old, {exchanged_token, _claims}} <-
|
||||
Auth.Guardian.exchange(refresh_token, ["access", "refresh"], "access"),
|
||||
{:ok, refresh_token} <- Users.generate_refresh_token(user) do
|
||||
{:ok, refresh_token} <- Authenticator.generate_refresh_token(user) do
|
||||
{:ok, %{access_token: exchanged_token, refresh_token: refresh_token}}
|
||||
else
|
||||
{:error, message} ->
|
||||
@ -151,7 +150,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
{:check_confirmation_token, Email.User.check_confirmation_token(token)},
|
||||
{:get_actor, actor} <- {:get_actor, Users.get_actor_for_user(user)},
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
|
||||
Users.generate_tokens(user) do
|
||||
Authenticator.generate_tokens(user) do
|
||||
{:ok,
|
||||
%{
|
||||
access_token: access_token,
|
||||
@ -192,10 +191,15 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
def send_reset_password(_parent, args, _resolution) do
|
||||
with email <- Map.get(args, :email),
|
||||
{:ok, %User{locale: locale} = user} <- Users.get_user_by_email(email, true),
|
||||
{:can_reset_password, true} <-
|
||||
{:can_reset_password, Authenticator.can_reset_password?(user)},
|
||||
{:ok, %Bamboo.Email{} = _email_html} <-
|
||||
Email.User.send_password_reset_email(user, Map.get(args, :locale, locale)) do
|
||||
{:ok, email}
|
||||
else
|
||||
{:can_reset_password, false} ->
|
||||
{:error, "This user can't reset their password"}
|
||||
|
||||
{:error, :user_not_found} ->
|
||||
# TODO : implement rate limits for this endpoint
|
||||
{:error, "No user with this email was found"}
|
||||
@ -209,10 +213,10 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
Reset the password from an user
|
||||
"""
|
||||
def reset_password(_parent, %{password: password, token: token}, _resolution) do
|
||||
with {:ok, %User{} = user} <-
|
||||
with {:ok, %User{email: email} = user} <-
|
||||
Email.User.check_reset_password_token(password, token),
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
|
||||
Users.authenticate(%{user: user, password: password}) do
|
||||
Authenticator.authenticate(email, password) do
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}}
|
||||
end
|
||||
end
|
||||
@ -295,10 +299,12 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
def change_password(
|
||||
_parent,
|
||||
%{old_password: old_password, new_password: new_password},
|
||||
%{context: %{current_user: %User{password_hash: old_password_hash} = user}}
|
||||
%{context: %{current_user: %User{} = user}}
|
||||
) do
|
||||
with {:current_password, true} <-
|
||||
{:current_password, Argon2.verify_pass(old_password, old_password_hash)},
|
||||
with {:can_change_password, true} <-
|
||||
{:can_change_password, Authenticator.can_change_password?(user)},
|
||||
{:current_password, {:ok, %User{}}} <-
|
||||
{:current_password, Authenticator.login(user.email, old_password)},
|
||||
{:same_password, false} <- {:same_password, old_password == new_password},
|
||||
{:ok, %User{} = user} <-
|
||||
user
|
||||
@ -306,7 +312,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
|> Repo.update() do
|
||||
{:ok, user}
|
||||
else
|
||||
{:current_password, false} ->
|
||||
{:current_password, _} ->
|
||||
{:error, "The current password is invalid"}
|
||||
|
||||
{:same_password, true} ->
|
||||
@ -323,10 +329,12 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
end
|
||||
|
||||
def change_email(_parent, %{email: new_email, password: password}, %{
|
||||
context: %{current_user: %User{email: old_email, password_hash: password_hash} = user}
|
||||
context: %{current_user: %User{email: old_email} = user}
|
||||
}) do
|
||||
with {:current_password, true} <-
|
||||
{:current_password, Argon2.verify_pass(password, password_hash)},
|
||||
with {:can_change_password, true} <-
|
||||
{:can_change_password, Authenticator.can_change_email?(user)},
|
||||
{:current_password, {:ok, %User{}}} <-
|
||||
{:current_password, Authenticator.login(user.email, password)},
|
||||
{:same_email, false} <- {:same_email, new_email == old_email},
|
||||
{:email_valid, true} <- {:email_valid, Email.Checker.valid?(new_email)},
|
||||
{:ok, %User{} = user} <-
|
||||
@ -347,7 +355,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
|
||||
{:ok, user}
|
||||
else
|
||||
{:current_password, false} ->
|
||||
{:current_password, _} ->
|
||||
{:error, "The password provided is invalid"}
|
||||
|
||||
{:same_email, true} ->
|
||||
@ -377,14 +385,24 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
end
|
||||
end
|
||||
|
||||
def delete_account(_parent, %{password: password}, %{
|
||||
context: %{current_user: %User{password_hash: password_hash} = user}
|
||||
def delete_account(_parent, args, %{
|
||||
context: %{current_user: %User{email: email} = user}
|
||||
}) do
|
||||
case {:current_password, Argon2.verify_pass(password, password_hash)} do
|
||||
{:current_password, true} ->
|
||||
with {:user_has_password, true} <- {:user_has_password, Authenticator.has_password?(user)},
|
||||
{:confirmation_password, password} when not is_nil(password) <-
|
||||
{:confirmation_password, Map.get(args, :password)},
|
||||
{:current_password, {:ok, _}} <-
|
||||
{:current_password, Authenticator.authenticate(email, password)} do
|
||||
do_delete_account(user)
|
||||
else
|
||||
# If the user hasn't got any password (3rd-party auth)
|
||||
{:user_has_password, false} ->
|
||||
do_delete_account(user)
|
||||
|
||||
{:current_password, false} ->
|
||||
{:confirmation_password, nil} ->
|
||||
{:error, "The password provided is invalid"}
|
||||
|
||||
{:current_password, _} ->
|
||||
{:error, "The password provided is invalid"}
|
||||
end
|
||||
end
|
||||
|
@ -39,6 +39,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
|
||||
end
|
||||
|
||||
field(:rules, :string, description: "The instance's rules")
|
||||
field(:auth, :auth, description: "The instance auth methods")
|
||||
end
|
||||
|
||||
object :terms do
|
||||
@ -132,6 +133,16 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
|
||||
field(:groups, :boolean)
|
||||
end
|
||||
|
||||
object :auth do
|
||||
field(:ldap, :boolean, description: "Whether or not LDAP auth is enabled")
|
||||
field(:oauth_providers, list_of(:oauth_provider), description: "List of oauth providers")
|
||||
end
|
||||
|
||||
object :oauth_provider do
|
||||
field(:id, :string, description: "The provider ID")
|
||||
field(:label, :string, description: "The label for the auth provider")
|
||||
end
|
||||
|
||||
object :config_queries do
|
||||
@desc "Get the instance config"
|
||||
field :config, :config do
|
||||
|
@ -52,6 +52,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
|
||||
|
||||
field(:locale, :string, description: "The user's locale")
|
||||
|
||||
field(:provider, :string, description: "The user's login provider")
|
||||
|
||||
field(:disabled, :boolean, description: "Whether the user is disabled")
|
||||
|
||||
field(:participations, :paginated_participant_list,
|
||||
|
@ -13,6 +13,7 @@ defmodule Mobilizon.Actors do
|
||||
alias Mobilizon.Media.File
|
||||
alias Mobilizon.Service.Workers
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
|
||||
@ -189,14 +190,19 @@ defmodule Mobilizon.Actors do
|
||||
Creates a new person actor.
|
||||
"""
|
||||
@spec new_person(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
|
||||
def new_person(args) do
|
||||
def new_person(args, default_actor \\ false) do
|
||||
args = Map.put(args, :keys, Crypto.generate_rsa_2048_private_key())
|
||||
|
||||
with {:ok, %Actor{} = person} <-
|
||||
with {:ok, %Actor{id: person_id} = person} <-
|
||||
%Actor{}
|
||||
|> Actor.registration_changeset(args)
|
||||
|> Repo.insert() do
|
||||
Events.create_feed_token(%{user_id: args["user_id"], actor_id: person.id})
|
||||
Events.create_feed_token(%{user_id: args.user_id, actor_id: person.id})
|
||||
|
||||
if default_actor do
|
||||
user = Users.get_user!(args.user_id)
|
||||
Users.update_user(user, %{default_actor_id: person_id})
|
||||
end
|
||||
|
||||
{:ok, person}
|
||||
end
|
||||
|
@ -186,6 +186,24 @@ defmodule Mobilizon.Config do
|
||||
def anonymous_reporting?,
|
||||
do: Application.get_env(:mobilizon, :anonymous)[:reports][:allowed]
|
||||
|
||||
@spec oauth_consumer_strategies() :: list({atom(), String.t()})
|
||||
def oauth_consumer_strategies do
|
||||
[:auth, :oauth_consumer_strategies]
|
||||
|> get([])
|
||||
|> Enum.map(fn strategy ->
|
||||
case strategy do
|
||||
{id, label} when is_atom(id) -> %{id: id, label: label}
|
||||
id when is_atom(id) -> %{id: id, label: nil}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@spec oauth_consumer_enabled? :: boolean()
|
||||
def oauth_consumer_enabled?, do: oauth_consumer_strategies() != []
|
||||
|
||||
@spec ldap_enabled? :: boolean()
|
||||
def ldap_enabled?, do: get([:ldap, :enabled], false)
|
||||
|
||||
def instance_resource_providers do
|
||||
types = get_in(Application.get_env(:mobilizon, Mobilizon.Service.ResourceProviders), [:types])
|
||||
|
||||
|
@ -40,14 +40,18 @@ defmodule Mobilizon.Users.User do
|
||||
:confirmation_token,
|
||||
:reset_password_sent_at,
|
||||
:reset_password_token,
|
||||
:default_actor_id,
|
||||
:locale,
|
||||
:unconfirmed_email,
|
||||
:disabled
|
||||
:disabled,
|
||||
:provider
|
||||
]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@registration_required_attrs @required_attrs ++ [:password]
|
||||
|
||||
@auth_provider_required_attrs @required_attrs ++ [:provider]
|
||||
|
||||
@password_change_required_attrs [:password]
|
||||
@password_reset_required_attrs @password_change_required_attrs ++
|
||||
[:reset_password_token, :reset_password_sent_at]
|
||||
@ -67,6 +71,7 @@ defmodule Mobilizon.Users.User do
|
||||
field(:unconfirmed_email, :string)
|
||||
field(:locale, :string, default: "en")
|
||||
field(:disabled, :boolean, default: false)
|
||||
field(:provider, :string)
|
||||
|
||||
belongs_to(:default_actor, Actor)
|
||||
has_many(:actors, Actor)
|
||||
@ -116,6 +121,16 @@ defmodule Mobilizon.Users.User do
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec auth_provider_changeset(t, map) :: Ecto.Changeset.t()
|
||||
def auth_provider_changeset(%__MODULE__{} = user, attrs) do
|
||||
user
|
||||
|> changeset(attrs)
|
||||
|> cast_assoc(:default_actor)
|
||||
|> put_change(:confirmed_at, DateTime.utc_now() |> DateTime.truncate(:second))
|
||||
|> validate_required(@auth_provider_required_attrs)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec send_password_reset_changeset(t, map) :: Ecto.Changeset.t()
|
||||
def send_password_reset_changeset(%__MODULE__{} = user, attrs) do
|
||||
|
@ -15,13 +15,6 @@ defmodule Mobilizon.Users do
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users.{Setting, User}
|
||||
|
||||
alias Mobilizon.Web.Auth
|
||||
|
||||
@type tokens :: %{
|
||||
required(:access_token) => String.t(),
|
||||
required(:refresh_token) => String.t()
|
||||
}
|
||||
|
||||
defenum(UserRole, :user_role, [:administrator, :moderator, :user])
|
||||
|
||||
defenum(NotificationPendingNotificationDelay, none: 0, direct: 1, one_hour: 5, one_day: 10)
|
||||
@ -41,6 +34,18 @@ defmodule Mobilizon.Users do
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_external(String.t(), String.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create_external(email, provider) do
|
||||
with {:ok, %User{} = user} <-
|
||||
%User{}
|
||||
|> User.auth_provider_changeset(%{email: email, provider: provider})
|
||||
|> Repo.insert() do
|
||||
Events.create_feed_token(%{user_id: user.id})
|
||||
|
||||
{:ok, user}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single user.
|
||||
Raises `Ecto.NoResultsError` if the user does not exist.
|
||||
@ -75,6 +80,16 @@ defmodule Mobilizon.Users do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets an user by its email.
|
||||
"""
|
||||
@spec get_user_by_email!(String.t(), boolean | nil) :: User.t()
|
||||
def get_user_by_email!(email, activated \\ nil) do
|
||||
email
|
||||
|> user_by_email_query(activated)
|
||||
|> Repo.one!()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get an user by its activation token.
|
||||
"""
|
||||
@ -267,52 +282,6 @@ defmodule Mobilizon.Users do
|
||||
@spec count_users :: integer
|
||||
def count_users, do: Repo.one(from(u in User, select: count(u.id)))
|
||||
|
||||
@doc """
|
||||
Authenticate an user.
|
||||
"""
|
||||
@spec authenticate(User.t()) :: {:ok, tokens} | {:error, :unauthorized}
|
||||
def authenticate(%{user: %User{password_hash: password_hash} = user, password: password}) do
|
||||
# Does password match the one stored in the database?
|
||||
if Argon2.verify_pass(password, password_hash) do
|
||||
{:ok, _tokens} = generate_tokens(user)
|
||||
else
|
||||
{:error, :unauthorized}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates access token and refresh token for an user.
|
||||
"""
|
||||
@spec generate_tokens(User.t()) :: {:ok, tokens}
|
||||
def generate_tokens(user) do
|
||||
with {:ok, access_token} <- generate_access_token(user),
|
||||
{:ok, refresh_token} <- generate_refresh_token(user) do
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates access token for an user.
|
||||
"""
|
||||
@spec generate_access_token(User.t()) :: {:ok, String.t()}
|
||||
def generate_access_token(user) do
|
||||
with {:ok, access_token, _claims} <-
|
||||
Auth.Guardian.encode_and_sign(user, %{}, token_type: "access") do
|
||||
{:ok, access_token}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates refresh token for an user.
|
||||
"""
|
||||
@spec generate_refresh_token(User.t()) :: {:ok, String.t()}
|
||||
def generate_refresh_token(user) do
|
||||
with {:ok, refresh_token, _claims} <-
|
||||
Auth.Guardian.encode_and_sign(user, %{}, token_type: "refresh") do
|
||||
{:ok, refresh_token}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a settings for an user.
|
||||
|
||||
|
93
lib/service/auth/authenticator.ex
Normal file
93
lib/service/auth/authenticator.ex
Normal file
@ -0,0 +1,93 @@
|
||||
defmodule Mobilizon.Service.Auth.Authenticator do
|
||||
@moduledoc """
|
||||
Module to handle authentification (currently through database or LDAP)
|
||||
"""
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Web.Auth.Guardian
|
||||
|
||||
@type tokens :: %{
|
||||
required(:access_token) => String.t(),
|
||||
required(:refresh_token) => String.t()
|
||||
}
|
||||
|
||||
@type tokens_with_user :: %{
|
||||
required(:access_token) => String.t(),
|
||||
required(:refresh_token) => String.t(),
|
||||
required(:user) => User.t()
|
||||
}
|
||||
|
||||
def implementation do
|
||||
Mobilizon.Config.get(
|
||||
Mobilizon.Service.Auth.Authenticator,
|
||||
Mobilizon.Service.Auth.MobilizonAuthenticator
|
||||
)
|
||||
end
|
||||
|
||||
@callback login(String.t(), String.t()) :: {:ok, User.t()} | {:error, any()}
|
||||
@spec login(String.t(), String.t()) :: {:ok, User.t()} | {:error, any()}
|
||||
def login(email, password), do: implementation().login(email, password)
|
||||
|
||||
@callback can_change_email?(User.t()) :: boolean
|
||||
def can_change_email?(%User{} = user), do: implementation().can_change_email?(user)
|
||||
|
||||
@callback can_change_password?(User.t()) :: boolean
|
||||
def can_change_password?(%User{} = user), do: implementation().can_change_password?(user)
|
||||
|
||||
@spec has_password?(User.t()) :: boolean()
|
||||
def has_password?(%User{provider: provider}), do: is_nil(provider) or provider == "ldap"
|
||||
|
||||
@spec can_reset_password?(User.t()) :: boolean()
|
||||
def can_reset_password?(%User{} = user), do: has_password?(user) && can_change_password?(user)
|
||||
|
||||
@spec authenticate(String.t(), String.t()) :: {:ok, tokens_with_user()}
|
||||
def authenticate(email, password) do
|
||||
with {:ok, %User{} = user} <- login(email, password),
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
|
||||
generate_tokens(user) do
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token, user: user}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates access token and refresh token for an user.
|
||||
"""
|
||||
@spec generate_tokens(User.t()) :: {:ok, tokens}
|
||||
def generate_tokens(user) do
|
||||
with {:ok, access_token} <- generate_access_token(user),
|
||||
{:ok, refresh_token} <- generate_refresh_token(user) do
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates access token for an user.
|
||||
"""
|
||||
@spec generate_access_token(User.t()) :: {:ok, String.t()}
|
||||
def generate_access_token(user) do
|
||||
with {:ok, access_token, _claims} <-
|
||||
Guardian.encode_and_sign(user, %{}, token_type: "access") do
|
||||
{:ok, access_token}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates refresh token for an user.
|
||||
"""
|
||||
@spec generate_refresh_token(User.t()) :: {:ok, String.t()}
|
||||
def generate_refresh_token(user) do
|
||||
with {:ok, refresh_token, _claims} <-
|
||||
Guardian.encode_and_sign(user, %{}, token_type: "refresh") do
|
||||
{:ok, refresh_token}
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_user(String.t()) :: User.t() | {:error, :user_not_found}
|
||||
def fetch_user(nil), do: {:error, :user_not_found}
|
||||
|
||||
def fetch_user(email) when not is_nil(email) do
|
||||
with {:ok, %User{} = user} <- Users.get_user_by_email(email, true) do
|
||||
user
|
||||
end
|
||||
end
|
||||
end
|
180
lib/service/auth/ldap_authenticator.ex
Normal file
180
lib/service/auth/ldap_authenticator.ex
Normal file
@ -0,0 +1,180 @@
|
||||
# Portions of this file are derived from Pleroma:
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Mobilizon.Service.Auth.LDAPAuthenticator do
|
||||
@moduledoc """
|
||||
Authenticate Mobilizon users through LDAP accounts
|
||||
"""
|
||||
alias Mobilizon.Service.Auth.{Authenticator, MobilizonAuthenticator}
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
require Logger
|
||||
|
||||
import Authenticator,
|
||||
only: [fetch_user: 1]
|
||||
|
||||
@behaviour Authenticator
|
||||
@base MobilizonAuthenticator
|
||||
|
||||
@connection_timeout 10_000
|
||||
@search_timeout 10_000
|
||||
|
||||
def login(email, password) do
|
||||
with {:ldap, true} <- {:ldap, Mobilizon.Config.get([:ldap, :enabled])},
|
||||
%User{} = user <- ldap_user(email, password) do
|
||||
{:ok, user}
|
||||
else
|
||||
{:error, {:ldap_connection_error, _}} ->
|
||||
# When LDAP is unavailable, try default authenticator
|
||||
@base.login(email, password)
|
||||
|
||||
{:ldap, _} ->
|
||||
@base.login(email, password)
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
def can_change_email?(%User{provider: provider}), do: provider != "ldap"
|
||||
|
||||
def can_change_password?(%User{provider: provider}), do: provider != "ldap"
|
||||
|
||||
defp ldap_user(email, password) do
|
||||
ldap = Mobilizon.Config.get(:ldap, [])
|
||||
host = Keyword.get(ldap, :host, "localhost")
|
||||
port = Keyword.get(ldap, :port, 389)
|
||||
ssl = Keyword.get(ldap, :ssl, false)
|
||||
sslopts = Keyword.get(ldap, :sslopts, [])
|
||||
|
||||
options =
|
||||
[{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}] ++
|
||||
if sslopts != [], do: [{:sslopts, sslopts}], else: []
|
||||
|
||||
case :eldap.open([to_charlist(host)], options) do
|
||||
{:ok, connection} ->
|
||||
try do
|
||||
ensure_eventual_tls(connection, ldap)
|
||||
|
||||
base = Keyword.get(ldap, :base)
|
||||
uid_field = Keyword.get(ldap, :uid, "cn")
|
||||
|
||||
# We first need to find the LDAP UID/CN for this specif email
|
||||
with uid when is_binary(uid) <- search_user(connection, ldap, base, uid_field, email),
|
||||
# Then we can verify the user's password
|
||||
:ok <- bind_user(connection, base, uid_field, uid, password) do
|
||||
case fetch_user(email) do
|
||||
%User{} = user ->
|
||||
user
|
||||
|
||||
_ ->
|
||||
register_user(email)
|
||||
end
|
||||
else
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
error ->
|
||||
{:error, error}
|
||||
end
|
||||
after
|
||||
:eldap.close(connection)
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("Could not open LDAP connection: #{inspect(error)}")
|
||||
{:error, {:ldap_connection_error, error}}
|
||||
end
|
||||
end
|
||||
|
||||
@spec bind_user(any(), String.t(), String.t(), String.t(), String.t()) ::
|
||||
User.t() | any()
|
||||
defp bind_user(connection, base, uid, field, password) do
|
||||
bind = "#{uid}=#{field},#{base}"
|
||||
Logger.debug("Binding to LDAP with \"#{bind}\"")
|
||||
:eldap.simple_bind(connection, bind, password)
|
||||
end
|
||||
|
||||
@spec search_user(any(), Keyword.t(), String.t(), String.t(), String.t()) ::
|
||||
String.t() | {:error, :ldap_registration_missing_attributes} | any()
|
||||
defp search_user(connection, ldap, base, uid, email) do
|
||||
# We may need to bind before performing the search
|
||||
res =
|
||||
if Keyword.get(ldap, :require_bind_for_search, true) do
|
||||
admin_field = Keyword.get(ldap, :bind_uid)
|
||||
admin_password = Keyword.get(ldap, :bind_password)
|
||||
bind_user(connection, base, uid, admin_field, admin_password)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
|
||||
if res == :ok do
|
||||
do_search_user(connection, base, uid, email)
|
||||
else
|
||||
res
|
||||
end
|
||||
end
|
||||
|
||||
# Search an user by uid to find their CN
|
||||
@spec do_search_user(any(), String.t(), String.t(), String.t()) ::
|
||||
String.t() | {:error, :ldap_registration_missing_attributes} | any()
|
||||
defp do_search_user(connection, base, uid, email) do
|
||||
with {:ok, {:eldap_search_result, [{:eldap_entry, _, attributes}], _}} <-
|
||||
:eldap.search(connection, [
|
||||
{:base, to_charlist(base)},
|
||||
{:filter, :eldap.equalityMatch(to_charlist("mail"), to_charlist(email))},
|
||||
{:scope, :eldap.wholeSubtree()},
|
||||
{:attributes, [to_charlist(uid)]},
|
||||
{:timeout, @search_timeout}
|
||||
]),
|
||||
{:uid, {_, [uid]}} <- {:uid, List.keyfind(attributes, to_charlist(uid), 0)} do
|
||||
:erlang.list_to_binary(uid)
|
||||
else
|
||||
{:ok, {:eldap_search_result, [], []}} ->
|
||||
Logger.info("Unable to find user with email #{email}")
|
||||
{:error, :ldap_search_email_not_found}
|
||||
|
||||
{:cn, err} ->
|
||||
Logger.error("Could not find LDAP attribute CN: #{inspect(err)}")
|
||||
{:error, :ldap_searcy_missing_attributes}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@spec register_user(String.t()) :: User.t() | any()
|
||||
defp register_user(email) do
|
||||
case Users.create_external(email, "ldap") do
|
||||
{:ok, %User{} = user} ->
|
||||
user
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@spec ensure_eventual_tls(any(), Keyword.t()) :: :ok
|
||||
defp ensure_eventual_tls(connection, ldap) do
|
||||
if Keyword.get(ldap, :tls, false) do
|
||||
:application.ensure_all_started(:ssl)
|
||||
|
||||
case :eldap.start_tls(
|
||||
connection,
|
||||
Keyword.get(ldap, :tlsopts, []),
|
||||
@connection_timeout
|
||||
) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
error ->
|
||||
Logger.error("Could not start TLS: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
39
lib/service/auth/mobilizon_authenticator.ex
Normal file
39
lib/service/auth/mobilizon_authenticator.ex
Normal file
@ -0,0 +1,39 @@
|
||||
defmodule Mobilizon.Service.Auth.MobilizonAuthenticator do
|
||||
@moduledoc """
|
||||
Authenticate Mobilizon users through database accounts
|
||||
"""
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
alias Mobilizon.Service.Auth.Authenticator
|
||||
|
||||
import Authenticator,
|
||||
only: [fetch_user: 1]
|
||||
|
||||
@behaviour Authenticator
|
||||
|
||||
def login(email, password) do
|
||||
require Logger
|
||||
|
||||
with {:user, %User{password_hash: password_hash, provider: nil} = user}
|
||||
when not is_nil(password_hash) <-
|
||||
{:user, fetch_user(email)},
|
||||
{:acceptable_password, true} <-
|
||||
{:acceptable_password, not (is_nil(password) || password == "")},
|
||||
{:checkpw, true} <- {:checkpw, Argon2.verify_pass(password, password_hash)} do
|
||||
{:ok, user}
|
||||
else
|
||||
{:user, {:error, :user_not_found}} ->
|
||||
{:error, :user_not_found}
|
||||
|
||||
{:acceptable_password, false} ->
|
||||
{:error, :bad_password}
|
||||
|
||||
{:checkpw, false} ->
|
||||
{:error, :bad_password}
|
||||
end
|
||||
end
|
||||
|
||||
def can_change_email?(%User{provider: provider}), do: is_nil(provider)
|
||||
|
||||
def can_change_password?(%User{provider: provider}), do: is_nil(provider)
|
||||
end
|
82
lib/web/controllers/auth_controller.ex
Normal file
82
lib/web/controllers/auth_controller.ex
Normal file
@ -0,0 +1,82 @@
|
||||
defmodule Mobilizon.Web.AuthController do
|
||||
use Mobilizon.Web, :controller
|
||||
|
||||
alias Mobilizon.Service.Auth.Authenticator
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Users.User
|
||||
require Logger
|
||||
plug(:put_layout, false)
|
||||
|
||||
plug(Ueberauth)
|
||||
|
||||
def request(conn, %{"provider" => provider} = _params) do
|
||||
redirect(conn, to: "/login?code=Login Provider not found&provider=#{provider}")
|
||||
end
|
||||
|
||||
def callback(
|
||||
%{assigns: %{ueberauth_failure: fails}} = conn,
|
||||
%{"provider" => provider} = _params
|
||||
) do
|
||||
Logger.warn("Unable to login user with #{provider} #{inspect(fails)}")
|
||||
|
||||
redirect(conn, to: "/login?code=Error with Login Provider&provider=#{provider}")
|
||||
end
|
||||
|
||||
def callback(
|
||||
%{assigns: %{ueberauth_auth: %Ueberauth.Auth{strategy: strategy} = auth}} = conn,
|
||||
_params
|
||||
) do
|
||||
email = email_from_ueberauth(auth)
|
||||
[_, _, _, strategy] = strategy |> to_string() |> String.split(".")
|
||||
strategy = String.downcase(strategy)
|
||||
|
||||
user =
|
||||
with {:valid_email, false} <- {:valid_email, is_nil(email) or email == ""},
|
||||
{:error, :user_not_found} <- Users.get_user_by_email(email),
|
||||
{:ok, %User{} = user} <- Users.create_external(email, strategy) do
|
||||
user
|
||||
else
|
||||
{:ok, %User{} = user} ->
|
||||
user
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
|
||||
error ->
|
||||
{:error, error}
|
||||
end
|
||||
|
||||
with %User{} = user <- user,
|
||||
{:ok, %{access_token: access_token, refresh_token: refresh_token}} <-
|
||||
Authenticator.generate_tokens(user) do
|
||||
Logger.info("Logged-in user \"#{email}\" through #{strategy}")
|
||||
|
||||
render(conn, "callback.html", %{
|
||||
access_token: access_token,
|
||||
refresh_token: refresh_token,
|
||||
user: user
|
||||
})
|
||||
else
|
||||
err ->
|
||||
Logger.warn("Unable to login user \"#{email}\" #{inspect(err)}")
|
||||
redirect(conn, to: "/login?code=Error with Login Provider&provider=#{strategy}")
|
||||
end
|
||||
end
|
||||
|
||||
# Github only give public emails as part of the user profile,
|
||||
# so we explicitely request all user emails and filter on the primary one
|
||||
defp email_from_ueberauth(%Ueberauth.Auth{
|
||||
strategy: Ueberauth.Strategy.Github,
|
||||
extra: %Ueberauth.Auth.Extra{raw_info: %{user: %{"emails" => emails}}}
|
||||
})
|
||||
when length(emails) > 0,
|
||||
do: emails |> Enum.find(& &1["primary"]) |> (& &1["email"]).()
|
||||
|
||||
defp email_from_ueberauth(%Ueberauth.Auth{
|
||||
extra: %Ueberauth.Auth.Extra{raw_info: %{user: %{"email" => email}}}
|
||||
})
|
||||
when not is_nil(email) and email != "",
|
||||
do: email
|
||||
|
||||
defp email_from_ueberauth(_), do: nil
|
||||
end
|
@ -150,6 +150,10 @@ defmodule Mobilizon.Web.Router do
|
||||
get("/groups/me", PageController, :index, as: "my_groups")
|
||||
|
||||
get("/interact", PageController, :interact)
|
||||
|
||||
get("/auth/:provider", AuthController, :request)
|
||||
get("/auth/:provider/callback", AuthController, :callback)
|
||||
post("/auth/:provider/callback", AuthController, :callback)
|
||||
end
|
||||
|
||||
scope "/proxy/", Mobilizon.Web do
|
||||
|
29
lib/web/views/auth_view.ex
Normal file
29
lib/web/views/auth_view.ex
Normal file
@ -0,0 +1,29 @@
|
||||
defmodule Mobilizon.Web.AuthView do
|
||||
@moduledoc """
|
||||
View for the auth routes
|
||||
"""
|
||||
|
||||
use Mobilizon.Web, :view
|
||||
alias Mobilizon.Service.Metadata.Instance
|
||||
alias Phoenix.HTML.Tag
|
||||
import Mobilizon.Web.Views.Utils
|
||||
|
||||
def render("callback.html", %{
|
||||
conn: conn,
|
||||
access_token: access_token,
|
||||
refresh_token: refresh_token,
|
||||
user: %{id: user_id, email: user_email, role: user_role, default_actor_id: user_actor_id}
|
||||
}) do
|
||||
info_tags = [
|
||||
Tag.tag(:meta, name: "auth-access-token", content: access_token),
|
||||
Tag.tag(:meta, name: "auth-refresh-token", content: refresh_token),
|
||||
Tag.tag(:meta, name: "auth-user-id", content: user_id),
|
||||
Tag.tag(:meta, name: "auth-user-email", content: user_email),
|
||||
Tag.tag(:meta, name: "auth-user-role", content: user_role),
|
||||
Tag.tag(:meta, name: "auth-user-actor-id", content: user_actor_id)
|
||||
]
|
||||
|
||||
tags = Instance.build_tags() ++ info_tags
|
||||
inject_tags(tags, get_locale(conn))
|
||||
end
|
||||
end
|
29
mix.exs
29
mix.exs
@ -46,6 +46,23 @@ defmodule Mobilizon.Mixfile do
|
||||
defp elixirc_paths(:dev), do: ["lib", "test/support/factory.ex"]
|
||||
defp elixirc_paths(_), do: ["lib"]
|
||||
|
||||
# Specifies OAuth dependencies.
|
||||
defp oauth_deps do
|
||||
oauth_strategy_packages =
|
||||
System.get_env("OAUTH_CONSUMER_STRATEGIES")
|
||||
|> to_string()
|
||||
|> String.split()
|
||||
|> Enum.map(fn strategy_entry ->
|
||||
with [_strategy, dependency] <- String.split(strategy_entry, ":") do
|
||||
dependency
|
||||
else
|
||||
[strategy] -> "ueberauth_#{strategy}"
|
||||
end
|
||||
end)
|
||||
|
||||
for s <- oauth_strategy_packages, do: {String.to_atom(s), ">= 0.0.0"}
|
||||
end
|
||||
|
||||
# Specifies your project dependencies.
|
||||
#
|
||||
# Type `mix help deps` for examples and options.
|
||||
@ -104,6 +121,16 @@ defmodule Mobilizon.Mixfile do
|
||||
{:floki, "~> 0.26.0"},
|
||||
{:ip_reserved, "~> 0.1.0"},
|
||||
{:fast_sanitize, "~> 0.1"},
|
||||
{:ueberauth, "~> 0.6"},
|
||||
{:ueberauth_twitter, "~> 0.3"},
|
||||
{:ueberauth_github, "~> 0.7"},
|
||||
{:ueberauth_facebook, "~> 0.8"},
|
||||
{:ueberauth_discord, "~> 0.5"},
|
||||
{:ueberauth_google, "~> 0.9"},
|
||||
{:ueberauth_keycloak_strategy,
|
||||
git: "https://github.com/tcitworld/ueberauth_keycloak.git", branch: "upgrade-deps"},
|
||||
{:ueberauth_gitlab_strategy,
|
||||
git: "https://github.com/tcitworld/ueberauth_gitlab.git", branch: "upgrade-deps"},
|
||||
# Dev and test dependencies
|
||||
{:phoenix_live_reload, "~> 1.2", only: [:dev, :e2e]},
|
||||
{:ex_machina, "~> 2.3", only: [:dev, :test]},
|
||||
@ -116,7 +143,7 @@ defmodule Mobilizon.Mixfile do
|
||||
{:credo, "~> 1.4.0", only: [:dev, :test], runtime: false},
|
||||
{:mock, "~> 0.3.4", only: :test},
|
||||
{:elixir_feed_parser, "~> 2.1.0", only: :test}
|
||||
]
|
||||
] ++ oauth_deps()
|
||||
end
|
||||
|
||||
# Aliases are shortcuts or tasks specific to the current project.
|
||||
|
19
mix.lock
19
mix.lock
@ -31,12 +31,13 @@
|
||||
"elixir_feed_parser": {:hex, :elixir_feed_parser, "2.1.0", "bb96fb6422158dc7ad59de62ef211cc69d264acbbe63941a64a5dce97bbbc2e6", [:mix], [{:timex, "~> 3.4", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "2d3c62fe7b396ee3b73d7160bc8fadbd78bfe9597c98c7d79b3f1038d9cba28f"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
|
||||
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
|
||||
"esaml": {:git, "git://github.com/wrren/esaml.git", "2cace5778e4323216bcff2085ca9739e42a68a42", [branch: "ueberauth_saml"]},
|
||||
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm", "b14f1dc204321429479c569cfbe8fb287541184ed040956c8862cb7a677b8406"},
|
||||
"ex_cldr": {:hex, :ex_cldr, "2.16.1", "905b03c38b5fb51668a347f2e6b586bcb2c0816cd98f7d913104872c43cbc61f", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:cldr_utils, "~> 2.9", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "006e500769982e57e6f3e32cbc4664345f78b014bb5ff48ddc394d67c86c1a8d"},
|
||||
"ex_cldr_calendars": {:hex, :ex_cldr_calendars, "1.9.0", "ace1c57ba3850753c9ac6ddb89dc0c9a9e5e1c57ecad587e21c8925ad30a3838", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:earmark, "~> 1.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.13", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.0", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a4b07773e2a326474f44a6bc51fffbec634859a1bad5cc6e6eb55eba45115541"},
|
||||
"ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.5.0", "e369ae3c1cd5cd20aa20988b153fd2902b4ab08aec63ca8757d7104bdb79f867", [:mix], [{:ex_cldr, "~> 2.14", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ba16b1df60bcec52c986481bbdfa7cfaec899b610f869d2b3c5a9a8149f67668"},
|
||||
"ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.5.1", "9439d1c40cfd03c3d8f3f60f5d3e3f2c6eaf0fd714541d687531cce78cfb9909", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 1.8", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.15", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "62a2f8d41ec6e789137bbf3ac7c944885a8ef6b7ce475905d056d1805b482427"},
|
||||
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.15.0", "207843c6ddae802a2b5fd43eb95c4b65eae8a0a876ce23ae4413eb098b222977", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.15", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.5", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "3c6c220e03590f08e2f3cb4f3e0c2e1a78fe56a12229331edb952cbdc67935e1"},
|
||||
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.15.1", "dced7ffee69c4830593258b69b294adb4c65cf539e1d8ae0a4de31cfc8aa56a0", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.15", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.5", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "c6a4b69ef80b8ffbb6c8fb69a2b365ba542580e0f76a15d8c6ee9142bd1b97ea"},
|
||||
"ex_crypto": {:hex, :ex_crypto, "0.10.0", "af600a89b784b36613a989da6e998c1b200ff1214c3cfbaf8deca4aa2f0a1739", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "ccc7472cfe8a0f4565f97dce7e9280119bf15a5ea51c6535e5b65f00660cde1c"},
|
||||
"ex_doc": {:hex, :ex_doc, "0.22.1", "9bb6d51508778193a4ea90fa16eac47f8b67934f33f8271d5e1edec2dc0eee4c", [:mix], [{:earmark, "~> 1.4.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "d957de1b75cb9f78d3ee17820733dc4460114d8b1e11f7ee4fd6546e69b1db60"},
|
||||
"ex_ical": {:hex, :ex_ical, "0.2.0", "4b928b554614704016cc0c9ee226eb854da9327a1cc460457621ceacb1ac29a6", [:mix], [{:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "db76473b2ae0259e6633c6c479a5a4d8603f09497f55c88f9ef4d53d2b75befb"},
|
||||
@ -91,7 +92,10 @@
|
||||
"mock": {:hex, :mock, "0.3.5", "feb81f52b8dcf0a0d65001d2fec459f6b6a8c22562d94a965862f6cc066b5431", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "6fae404799408300f863550392635d8f7e3da6b71abdd5c393faf41b131c8728"},
|
||||
"mogrify": {:hex, :mogrify, "0.7.4", "9b2496dde44b1ce12676f85d7dc531900939e6367bc537c7243a1b089435b32d", [:mix], [], "hexpm", "50d79e337fba6bc95bfbef918058c90f50b17eed9537771e61d4619488f099c3"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
|
||||
"oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"},
|
||||
"oauther": {:hex, :oauther, "1.1.1", "7d8b16167bb587ecbcddd3f8792beb9ec3e7b65c1f8ebd86b8dd25318d535752", [:mix], [], "hexpm", "9374f4302045321874cccdc57eb975893643bd69c3b22bf1312dab5f06e5788e"},
|
||||
"oban": {:hex, :oban, "1.2.0", "7cca94d341be43d220571e28f69131c4afc21095b25257397f50973d3fc59b07", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ba5f8b3f7d76967b3e23cf8014f6a13e4ccb33431e4808f036709a7f822362ee"},
|
||||
"paddle": {:hex, :paddle, "0.1.4", "3697996d79e3d771d6f7560a23e4bad1ed7b7f7fd3e784f97bc39565963b2b13", [:mix], [], "hexpm", "fc719a9e7c86f319b9f4bf413d6f0f326b0c4930d5bc6630d074598ed38e2143"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
|
||||
"phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
|
||||
@ -110,10 +114,21 @@
|
||||
"sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"},
|
||||
"slugger": {:hex, :slugger, "0.3.0", "efc667ab99eee19a48913ccf3d038b1fb9f165fa4fbf093be898b8099e61b6ed", [:mix], [], "hexpm", "20d0ded0e712605d1eae6c5b4889581c3460d92623a930ddda91e0e609b5afba"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
|
||||
"timex": {:hex, :timex, "3.6.2", "845cdeb6119e2fef10751c0b247b6c59d86d78554c83f78db612e3290f819bc2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "26030b46199d02a590be61c2394b37ea25a3664c02fafbeca0b24c972025d47a"},
|
||||
"tzdata": {:hex, :tzdata, "1.0.3", "73470ad29dde46e350c60a66e6b360d3b99d2d18b74c4c349dbebbc27a09a3eb", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a6e1ee7003c4d04ecbd21dd3ec690d4c6662db5d3bbdd7262d53cdf5e7c746c1"},
|
||||
"ueberauth": {:hex, :ueberauth, "0.6.3", "d42ace28b870e8072cf30e32e385579c57b9cc96ec74fa1f30f30da9c14f3cc0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "afc293d8a1140d6591b53e3eaf415ca92842cb1d32fad3c450c6f045f7f91b60"},
|
||||
"ueberauth_discord": {:hex, :ueberauth_discord, "0.5.0", "52421277b93fda769b51636e542b5085f3861efdc7fa48ac4bedb6dae0b645e1", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.3", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "9a3808baf44297e26bd5042ba9ea5398aa60023e054eb9a5ac8a4eacd0467a78"},
|
||||
"ueberauth_facebook": {:hex, :ueberauth_facebook, "0.8.1", "c254be4ab367c276773c2e41d3c0fe343ae118e244afc8d5a4e3e5c438951fdc", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "c2cf210ef45bd20611234ef17517f9d1dff6b31d3fb6ad96789143eb0943f540"},
|
||||
"ueberauth_github": {:hex, :ueberauth_github, "0.8.0", "2216c8cdacee0de6245b422fb397921b64a29416526985304e345dab6a799d17", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "b65ccc001a7b0719ba069452f3333d68891f4613ae787a340cce31e2a43307a3"},
|
||||
"ueberauth_gitlab_strategy": {:git, "https://github.com/tcitworld/ueberauth_gitlab.git", "9fc5d30b5d87ff7cdef293a1c128f25777dcbe59", [branch: "upgrade-deps"]},
|
||||
"ueberauth_google": {:hex, :ueberauth_google, "0.9.0", "e098e1d6df647696b858b0289eae7e4dc8c662abee9e309d64bc115192c51bf5", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "5453ba074df7ee14fb5b121bb04a64cda5266cd23b28af8a2fdf02dd40959ab4"},
|
||||
"ueberauth_keycloak": {:git, "https://github.com/tcitworld/ueberauth_keycloak.git", "02447d8a75bd36ba26c17c7b1b8bab3538bb2e7a", [branch: "upgrade-deps"]},
|
||||
"ueberauth_keycloak_strategy": {:git, "https://github.com/tcitworld/ueberauth_keycloak.git", "d892f0f9daf9e0023319b69ac2f7c2c6edff2b14", [branch: "upgrade-deps"]},
|
||||
"ueberauth_saml": {:git, "https://github.com/wrren/ueberauth_saml.git", "dfcb4ae3f509afec0f442ce455c41feacac24511", []},
|
||||
"ueberauth_twitter": {:hex, :ueberauth_twitter, "0.4.0", "4b98620341bc91bac90459093bba093c650823b6e2df35b70255c493c17e9227", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:oauther, "~> 1.1", [hex: :oauther, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "fb29c9047ca263038c0c61f5a0ec8597e8564aba3f2b4cb02704b60205fd4468"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
|
||||
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm", "6c7729a2d214806450d29766abc2afaa7a2cbecf415be64f36a6691afebb50e5"},
|
||||
"uuid": {:git, "git://github.com/botsunit/erlang-uuid", "1effbbbd200f9f5d9d5154e81b83fe8e4c3fe714", [branch: "master"]},
|
||||
"xml_builder": {:hex, :xml_builder, "2.0.0", "371ed27bb63bf0598dbaf3f0c466e5dc7d16cb4ecb68f06a67f953654062e21b", [:mix], [], "hexpm", "baeb5c8d42204bac2b856ffd50e8cda42d63b622984538d18d92733e4e790fbd"},
|
||||
}
|
||||
|
@ -1177,12 +1177,12 @@ msgstr "If you didn't request this, please ignore this email."
|
||||
#, elixir-format, fuzzy
|
||||
#: lib/web/templates/email/email.text.eex:10
|
||||
msgid "In the meantime, please consider this the software as not (yet) finished. Read more on the Framasoft blog:"
|
||||
msgstr "In the meantime, please consider that the software is not (yet) finished. More information %{a_start}on our blog%{a_end}."
|
||||
msgstr "In the meantime, please consider that the software is not (yet) finished. More information on our blog."
|
||||
|
||||
#, elixir-format, fuzzy
|
||||
#: lib/web/templates/email/email.text.eex:9
|
||||
msgid "Mobilizon is still under development, we will add new features along the updates, until the release of version 1 of the software in the fall of 2020."
|
||||
msgstr "Mobilizon is under development, we will add new features to this site during regular updates, until the release of %{b_start}version 1 of the software in the first half of 2020%{b_end}."
|
||||
msgstr "Mobilizon is under development, we will add new features to this site during regular updates, until the release of version 1 of the software in the first half of 2020."
|
||||
|
||||
#, elixir-format, fuzzy
|
||||
#: lib/web/templates/email/email.text.eex:7
|
||||
|
@ -0,0 +1,17 @@
|
||||
defmodule Mobilizon.Storage.Repo.Migrations.AddProviderToUserAndMakePasswordMandatory do
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:users) do
|
||||
add(:provider, :string, null: true)
|
||||
modify(:password_hash, :string, null: true)
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:users) do
|
||||
remove(:provider)
|
||||
modify(:password_hash, :string, null: false)
|
||||
end
|
||||
end
|
||||
end
|
@ -991,7 +991,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ParticipantTest do
|
||||
}
|
||||
"""
|
||||
|
||||
clear_config([:anonymous, :participation])
|
||||
setup do: clear_config([:anonymous, :participation])
|
||||
|
||||
setup %{conn: conn, actor: actor, user: user} do
|
||||
Mobilizon.Config.clear_config_cache()
|
||||
|
@ -33,7 +33,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ReportTest do
|
||||
}
|
||||
"""
|
||||
|
||||
clear_config([:anonymous, :reports])
|
||||
setup do: clear_config([:anonymous, :reports])
|
||||
|
||||
setup %{conn: conn} do
|
||||
Mobilizon.Config.clear_config_cache()
|
||||
|
@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Service.Auth.Authenticator
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
alias Mobilizon.GraphQL.AbsintheHelpers
|
||||
@ -45,8 +46,14 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
||||
}
|
||||
"""
|
||||
|
||||
@send_reset_password_mutation """
|
||||
mutation SendResetPassword($email: String!) {
|
||||
sendResetPassword(email: $email)
|
||||
}
|
||||
"""
|
||||
|
||||
@delete_user_account_mutation """
|
||||
mutation DeleteAccount($password: String!) {
|
||||
mutation DeleteAccount($password: String) {
|
||||
deleteAccount (password: $password) {
|
||||
id
|
||||
}
|
||||
@ -712,45 +719,50 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
||||
end
|
||||
|
||||
describe "Resolver: Send reset password" do
|
||||
test "test send_reset_password/3 with valid email", context do
|
||||
user = insert(:user)
|
||||
|
||||
mutation = """
|
||||
mutation {
|
||||
sendResetPassword(
|
||||
email: "#{user.email}"
|
||||
)
|
||||
}
|
||||
"""
|
||||
test "test send_reset_password/3 with valid email", %{conn: conn} do
|
||||
%User{email: email} = insert(:user)
|
||||
|
||||
res =
|
||||
context.conn
|
||||
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @send_reset_password_mutation,
|
||||
variables: %{email: email}
|
||||
)
|
||||
|
||||
assert json_response(res, 200)["data"]["sendResetPassword"] == user.email
|
||||
assert res["data"]["sendResetPassword"] == email
|
||||
end
|
||||
|
||||
test "test send_reset_password/3 with invalid email", context do
|
||||
mutation = """
|
||||
mutation {
|
||||
sendResetPassword(
|
||||
email: "oh no"
|
||||
)
|
||||
}
|
||||
"""
|
||||
test "test send_reset_password/3 with invalid email", %{conn: conn} do
|
||||
res =
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @send_reset_password_mutation,
|
||||
variables: %{email: "not an email"}
|
||||
)
|
||||
|
||||
assert hd(res["errors"])["message"] ==
|
||||
"No user with this email was found"
|
||||
end
|
||||
|
||||
test "test send_reset_password/3 for an LDAP user", %{conn: conn} do
|
||||
{:ok, %User{email: email}} = Users.create_external("some@users.com", "ldap")
|
||||
|
||||
res =
|
||||
context.conn
|
||||
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @send_reset_password_mutation,
|
||||
variables: %{email: email}
|
||||
)
|
||||
|
||||
assert hd(json_response(res, 200)["errors"])["message"] ==
|
||||
"No user with this email was found"
|
||||
assert hd(res["errors"])["message"] ==
|
||||
"This user can't reset their password"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Resolver: Reset user's password" do
|
||||
test "test reset_password/3 with valid email", context do
|
||||
{:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"})
|
||||
Users.update_user(user, %{confirmed_at: DateTime.utc_now()})
|
||||
%Actor{} = insert(:actor, user: user)
|
||||
{:ok, _email_sent} = Email.User.send_password_reset_email(user)
|
||||
%User{reset_password_token: reset_password_token} = Users.get_user!(user.id)
|
||||
@ -772,6 +784,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
||||
context.conn
|
||||
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
||||
|
||||
assert is_nil(json_response(res, 200)["errors"])
|
||||
assert json_response(res, 200)["data"]["resetPassword"]["user"]["id"] == to_string(user.id)
|
||||
end
|
||||
|
||||
@ -829,7 +842,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
||||
end
|
||||
|
||||
describe "Resolver: Login a user" do
|
||||
test "test login_user/3 with valid credentials", context do
|
||||
test "test login_user/3 with valid credentials", %{conn: conn} do
|
||||
{:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"})
|
||||
|
||||
{:ok, %User{} = _user} =
|
||||
@ -839,30 +852,18 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
||||
"confirmation_token" => nil
|
||||
})
|
||||
|
||||
mutation = """
|
||||
mutation {
|
||||
login(
|
||||
email: "#{user.email}",
|
||||
password: "#{user.password}",
|
||||
) {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
user {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
res =
|
||||
context.conn
|
||||
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @login_mutation,
|
||||
variables: %{email: user.email, password: user.password}
|
||||
)
|
||||
|
||||
assert login = json_response(res, 200)["data"]["login"]
|
||||
assert login = res["data"]["login"]
|
||||
assert Map.has_key?(login, "accessToken") && not is_nil(login["accessToken"])
|
||||
end
|
||||
|
||||
test "test login_user/3 with invalid password", context do
|
||||
test "test login_user/3 with invalid password", %{conn: conn} do
|
||||
{:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"})
|
||||
|
||||
{:ok, %User{} = _user} =
|
||||
@ -872,79 +873,40 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
||||
"confirmation_token" => nil
|
||||
})
|
||||
|
||||
mutation = """
|
||||
mutation {
|
||||
login(
|
||||
email: "#{user.email}",
|
||||
password: "bad password",
|
||||
) {
|
||||
accessToken,
|
||||
user {
|
||||
default_actor {
|
||||
preferred_username,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
res =
|
||||
context.conn
|
||||
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @login_mutation,
|
||||
variables: %{email: user.email, password: "bad password"}
|
||||
)
|
||||
|
||||
assert hd(json_response(res, 200)["errors"])["message"] ==
|
||||
assert hd(res["errors"])["message"] ==
|
||||
"Impossible to authenticate, either your email or password are invalid."
|
||||
end
|
||||
|
||||
test "test login_user/3 with invalid email", context do
|
||||
mutation = """
|
||||
mutation {
|
||||
login(
|
||||
email: "bad email",
|
||||
password: "bad password",
|
||||
) {
|
||||
accessToken,
|
||||
user {
|
||||
default_actor {
|
||||
preferred_username,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
test "test login_user/3 with invalid email", %{conn: conn} do
|
||||
res =
|
||||
context.conn
|
||||
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @login_mutation,
|
||||
variables: %{email: "bad email", password: "bad password"}
|
||||
)
|
||||
|
||||
assert hd(json_response(res, 200)["errors"])["message"] ==
|
||||
assert hd(res["errors"])["message"] ==
|
||||
"No user with this email was found"
|
||||
end
|
||||
|
||||
test "test login_user/3 with unconfirmed user", context do
|
||||
test "test login_user/3 with unconfirmed user", %{conn: conn} do
|
||||
{:ok, %User{} = user} = Users.register(%{email: "toto@tata.tld", password: "p4ssw0rd"})
|
||||
|
||||
mutation = """
|
||||
mutation {
|
||||
login(
|
||||
email: "#{user.email}",
|
||||
password: "#{user.password}",
|
||||
) {
|
||||
accessToken,
|
||||
user {
|
||||
default_actor {
|
||||
preferred_username,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
res =
|
||||
context.conn
|
||||
|> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @login_mutation,
|
||||
variables: %{email: user.email, password: user.password}
|
||||
)
|
||||
|
||||
assert hd(json_response(res, 200)["errors"])["message"] == "User account not confirmed"
|
||||
assert hd(res["errors"])["message"] == "No user with this email was found"
|
||||
end
|
||||
end
|
||||
|
||||
@ -970,7 +932,7 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
||||
|
||||
test "test refresh_token/3 with an appropriate token", context do
|
||||
user = insert(:user)
|
||||
{:ok, refresh_token} = Users.generate_refresh_token(user)
|
||||
{:ok, refresh_token} = Authenticator.generate_refresh_token(user)
|
||||
|
||||
mutation = """
|
||||
mutation {
|
||||
@ -1441,6 +1403,18 @@ defmodule Mobilizon.GraphQL.Resolvers.UserTest do
|
||||
assert is_nil(Events.get_participant(participant_id))
|
||||
end
|
||||
|
||||
test "delete_account/3 with 3rd-party auth login", %{conn: conn} do
|
||||
{:ok, %User{} = user} = Users.create_external(@email, "keycloak")
|
||||
|
||||
res =
|
||||
conn
|
||||
|> auth_conn(user)
|
||||
|> AbsintheHelpers.graphql_query(query: @delete_user_account_mutation)
|
||||
|
||||
assert is_nil(res["errors"])
|
||||
assert res["data"]["deleteAccount"]["id"] == to_string(user.id)
|
||||
end
|
||||
|
||||
test "delete_account/3 with invalid password", %{conn: conn} do
|
||||
{:ok, %User{} = user} = Users.register(%{email: @email, password: @password})
|
||||
|
||||
|
@ -72,14 +72,6 @@ defmodule Mobilizon.UsersTest do
|
||||
|
||||
@email "email@domain.tld"
|
||||
@password "password"
|
||||
test "authenticate/1 checks the user's password" do
|
||||
{:ok, %User{} = user} = Users.register(%{email: @email, password: @password})
|
||||
|
||||
assert {:ok, _} = Users.authenticate(%{user: user, password: @password})
|
||||
|
||||
assert {:error, :unauthorized} ==
|
||||
Users.authenticate(%{user: user, password: "bad password"})
|
||||
end
|
||||
|
||||
test "get_user_by_email/1 finds an user by its email" do
|
||||
{:ok, %User{email: email} = user} = Users.register(%{email: @email, password: @password})
|
||||
|
34
test/service/auth/authentificator_test.exs
Normal file
34
test/service/auth/authentificator_test.exs
Normal file
@ -0,0 +1,34 @@
|
||||
defmodule Mobilizon.Service.Auth.AuthenticatorTest do
|
||||
use Mobilizon.DataCase
|
||||
|
||||
alias Mobilizon.Service.Auth.Authenticator
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Users.User
|
||||
import Mobilizon.Factory
|
||||
|
||||
@email "email@domain.tld"
|
||||
@password "password"
|
||||
|
||||
describe "test authentification" do
|
||||
test "authenticate/1 checks the user's password" do
|
||||
{:ok, %User{} = user} = Users.register(%{email: @email, password: @password})
|
||||
Users.update_user(user, %{confirmed_at: DateTime.utc_now()})
|
||||
|
||||
assert {:ok, _} = Authenticator.authenticate(@email, @password)
|
||||
|
||||
assert {:error, :bad_password} ==
|
||||
Authenticator.authenticate(@email, "completely wrong password")
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_user/1" do
|
||||
test "returns user by email" do
|
||||
user = insert(:user)
|
||||
assert Authenticator.fetch_user(user.email).id == user.id
|
||||
end
|
||||
|
||||
test "returns nil" do
|
||||
assert Authenticator.fetch_user("email") == {:error, :user_not_found}
|
||||
end
|
||||
end
|
||||
end
|
238
test/service/auth/ldap_authentificator_test.exs
Normal file
238
test/service/auth/ldap_authentificator_test.exs
Normal file
@ -0,0 +1,238 @@
|
||||
defmodule Mobilizon.Service.Auth.LDAPAuthenticatorTest do
|
||||
use Mobilizon.Web.ConnCase
|
||||
use Mobilizon.Tests.Helpers
|
||||
|
||||
alias Mobilizon.GraphQL.AbsintheHelpers
|
||||
alias Mobilizon.Service.Auth.{Authenticator, LDAPAuthenticator}
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Web.Auth.Guardian
|
||||
|
||||
import Mobilizon.Factory
|
||||
import ExUnit.CaptureLog
|
||||
import Mock
|
||||
|
||||
@skip if !Code.ensure_loaded?(:eldap), do: :skip
|
||||
@admin_password "admin_password"
|
||||
|
||||
setup_all do
|
||||
clear_config([:ldap, :enabled], true)
|
||||
clear_config([:ldap, :bind_uid], "admin")
|
||||
clear_config([:ldap, :bind_password], @admin_password)
|
||||
end
|
||||
|
||||
setup_all do:
|
||||
clear_config(
|
||||
Authenticator,
|
||||
LDAPAuthenticator
|
||||
)
|
||||
|
||||
@login_mutation """
|
||||
mutation Login($email: String!, $password: String!) {
|
||||
login(email: $email, password: $password) {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
user {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
describe "login" do
|
||||
@tag @skip
|
||||
test "authorizes the existing user using LDAP credentials", %{conn: conn} do
|
||||
user_password = "testpassword"
|
||||
admin_password = "admin_password"
|
||||
user = insert(:user, password_hash: Argon2.hash_pwd_salt(user_password))
|
||||
|
||||
host = [:ldap, :host] |> Mobilizon.Config.get() |> to_charlist
|
||||
port = Mobilizon.Config.get([:ldap, :port])
|
||||
|
||||
with_mocks [
|
||||
{:eldap, [],
|
||||
[
|
||||
open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end,
|
||||
simple_bind: fn _connection, _dn, password ->
|
||||
case password do
|
||||
^admin_password -> :ok
|
||||
^user_password -> :ok
|
||||
end
|
||||
end,
|
||||
equalityMatch: fn _type, _value -> :ok end,
|
||||
wholeSubtree: fn -> :ok end,
|
||||
search: fn _connection, _options ->
|
||||
{:ok,
|
||||
{:eldap_search_result, [{:eldap_entry, '', [{'cn', [to_charlist("MyUser")]}]}], []}}
|
||||
end,
|
||||
close: fn _connection ->
|
||||
send(self(), :close_connection)
|
||||
:ok
|
||||
end
|
||||
]}
|
||||
] do
|
||||
res =
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @login_mutation,
|
||||
variables: %{email: user.email, password: user_password}
|
||||
)
|
||||
|
||||
assert is_nil(res["error"])
|
||||
assert token = res["data"]["login"]["accessToken"]
|
||||
|
||||
{:ok, %User{} = user_from_token, _claims} = Guardian.resource_from_token(token)
|
||||
|
||||
assert user_from_token.id == user.id
|
||||
assert_received :close_connection
|
||||
end
|
||||
end
|
||||
|
||||
@tag @skip
|
||||
test "creates a new user after successful LDAP authorization", %{conn: conn} do
|
||||
user_password = "testpassword"
|
||||
admin_password = "admin_password"
|
||||
user = build(:user)
|
||||
|
||||
host = [:ldap, :host] |> Mobilizon.Config.get() |> to_charlist
|
||||
port = Mobilizon.Config.get([:ldap, :port])
|
||||
|
||||
with_mocks [
|
||||
{:eldap, [],
|
||||
[
|
||||
open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end,
|
||||
simple_bind: fn _connection, _dn, password ->
|
||||
case password do
|
||||
^admin_password -> :ok
|
||||
^user_password -> :ok
|
||||
end
|
||||
end,
|
||||
equalityMatch: fn _type, _value -> :ok end,
|
||||
wholeSubtree: fn -> :ok end,
|
||||
search: fn _connection, _options ->
|
||||
{:ok,
|
||||
{:eldap_search_result, [{:eldap_entry, '', [{'cn', [to_charlist("MyUser")]}]}], []}}
|
||||
end,
|
||||
close: fn _connection ->
|
||||
send(self(), :close_connection)
|
||||
:ok
|
||||
end
|
||||
]}
|
||||
] do
|
||||
res =
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @login_mutation,
|
||||
variables: %{email: user.email, password: user_password}
|
||||
)
|
||||
|
||||
assert is_nil(res["error"])
|
||||
assert token = res["data"]["login"]["accessToken"]
|
||||
|
||||
{:ok, %User{} = user_from_token, _claims} = Guardian.resource_from_token(token)
|
||||
|
||||
assert user_from_token.email == user.email
|
||||
assert_received :close_connection
|
||||
end
|
||||
end
|
||||
|
||||
@tag @skip
|
||||
test "falls back to the default authorization when LDAP is unavailable", %{conn: conn} do
|
||||
user_password = "testpassword"
|
||||
admin_password = "admin_password"
|
||||
user = insert(:user, password_hash: Argon2.hash_pwd_salt(user_password))
|
||||
|
||||
host = [:ldap, :host] |> Mobilizon.Config.get() |> to_charlist
|
||||
port = Mobilizon.Config.get([:ldap, :port])
|
||||
|
||||
with_mocks [
|
||||
{:eldap, [],
|
||||
[
|
||||
open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:error, 'connect failed'} end,
|
||||
simple_bind: fn _connection, _dn, password ->
|
||||
case password do
|
||||
^admin_password -> :ok
|
||||
^user_password -> :ok
|
||||
end
|
||||
end,
|
||||
equalityMatch: fn _type, _value -> :ok end,
|
||||
wholeSubtree: fn -> :ok end,
|
||||
search: fn _connection, _options ->
|
||||
{:ok,
|
||||
{:eldap_search_result, [{:eldap_entry, '', [{'cn', [to_charlist("MyUser")]}]}], []}}
|
||||
end,
|
||||
close: fn _connection ->
|
||||
send(self(), :close_connection)
|
||||
:ok
|
||||
end
|
||||
]}
|
||||
] do
|
||||
log =
|
||||
capture_log(fn ->
|
||||
res =
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @login_mutation,
|
||||
variables: %{email: user.email, password: user_password}
|
||||
)
|
||||
|
||||
assert is_nil(res["error"])
|
||||
assert token = res["data"]["login"]["accessToken"]
|
||||
|
||||
{:ok, %User{} = user_from_token, _claims} = Guardian.resource_from_token(token)
|
||||
|
||||
assert user_from_token.email == user.email
|
||||
end)
|
||||
|
||||
assert log =~ "Could not open LDAP connection: 'connect failed'"
|
||||
refute_received :close_connection
|
||||
end
|
||||
end
|
||||
|
||||
@tag @skip
|
||||
test "disallow authorization for wrong LDAP credentials", %{conn: conn} do
|
||||
user_password = "testpassword"
|
||||
user = insert(:user, password_hash: Argon2.hash_pwd_salt(user_password))
|
||||
|
||||
host = [:ldap, :host] |> Mobilizon.Config.get() |> to_charlist
|
||||
port = Mobilizon.Config.get([:ldap, :port])
|
||||
|
||||
with_mocks [
|
||||
{:eldap, [],
|
||||
[
|
||||
open: fn [^host], [{:port, ^port}, {:ssl, false} | _] -> {:ok, self()} end,
|
||||
simple_bind: fn _connection, _dn, _password -> {:error, :invalidCredentials} end,
|
||||
close: fn _connection ->
|
||||
send(self(), :close_connection)
|
||||
:ok
|
||||
end
|
||||
]}
|
||||
] do
|
||||
res =
|
||||
conn
|
||||
|> AbsintheHelpers.graphql_query(
|
||||
query: @login_mutation,
|
||||
variables: %{email: user.email, password: user_password}
|
||||
)
|
||||
|
||||
refute is_nil(res["errors"])
|
||||
|
||||
assert assert hd(res["errors"])["message"] ==
|
||||
"Impossible to authenticate, either your email or password are invalid."
|
||||
|
||||
assert_received :close_connection
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "can change" do
|
||||
test "password" do
|
||||
assert LDAPAuthenticator.can_change_password?(%User{provider: "ldap"}) == false
|
||||
assert LDAPAuthenticator.can_change_password?(%User{provider: nil}) == true
|
||||
end
|
||||
|
||||
test "email" do
|
||||
assert LDAPAuthenticator.can_change_password?(%User{provider: "ldap"}) == false
|
||||
assert LDAPAuthenticator.can_change_password?(%User{provider: nil}) == true
|
||||
end
|
||||
end
|
||||
end
|
29
test/service/auth/mobilizon_authentificator_test.exs
Normal file
29
test/service/auth/mobilizon_authentificator_test.exs
Normal file
@ -0,0 +1,29 @@
|
||||
defmodule Mobilizon.Service.Auth.MobilizonAuthenticatorTest do
|
||||
use Mobilizon.DataCase
|
||||
|
||||
alias Mobilizon.Service.Auth.MobilizonAuthenticator
|
||||
alias Mobilizon.Users.User
|
||||
import Mobilizon.Factory
|
||||
|
||||
setup do
|
||||
password = "testpassword"
|
||||
email = "someone@somewhere.tld"
|
||||
user = insert(:user, email: email, password_hash: Argon2.hash_pwd_salt(password))
|
||||
{:ok, [user: user, email: email, password: password]}
|
||||
end
|
||||
|
||||
test "login", %{email: email, password: password, user: user} do
|
||||
assert {:ok, %User{} = returned_user} = MobilizonAuthenticator.login(email, password)
|
||||
assert returned_user.id == user.id
|
||||
end
|
||||
|
||||
test "login with invalid password", %{email: email} do
|
||||
assert {:error, :bad_password} == MobilizonAuthenticator.login(email, "invalid")
|
||||
assert {:error, :bad_password} == MobilizonAuthenticator.login(email, nil)
|
||||
end
|
||||
|
||||
test "login with no credentials" do
|
||||
assert {:error, :user_not_found} == MobilizonAuthenticator.login("some@email.com", nil)
|
||||
assert {:error, :user_not_found} == MobilizonAuthenticator.login(nil, nil)
|
||||
end
|
||||
end
|
@ -18,7 +18,8 @@ defmodule Mobilizon.Factory do
|
||||
role: :user,
|
||||
confirmed_at: DateTime.utc_now() |> DateTime.truncate(:second),
|
||||
confirmation_sent_at: nil,
|
||||
confirmation_token: nil
|
||||
confirmation_token: nil,
|
||||
provider: nil
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -7,6 +7,7 @@ defmodule Mobilizon.Tests.Helpers do
|
||||
@moduledoc """
|
||||
Helpers for use in tests.
|
||||
"""
|
||||
alias Mobilizon.Config
|
||||
|
||||
defmacro clear_config(config_path) do
|
||||
quote do
|
||||
@ -17,11 +18,17 @@ defmodule Mobilizon.Tests.Helpers do
|
||||
|
||||
defmacro clear_config(config_path, do: yield) do
|
||||
quote do
|
||||
setup do
|
||||
initial_setting = Mobilizon.Config.get(unquote(config_path))
|
||||
unquote(yield)
|
||||
on_exit(fn -> Mobilizon.Config.put(unquote(config_path), initial_setting) end)
|
||||
:ok
|
||||
initial_setting = Config.get(unquote(config_path))
|
||||
unquote(yield)
|
||||
on_exit(fn -> Config.put(unquote(config_path), initial_setting) end)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defmacro clear_config(config_path, temp_setting) do
|
||||
quote do
|
||||
clear_config(unquote(config_path)) do
|
||||
Config.put(unquote(config_path), unquote(temp_setting))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
54
test/web/controllers/auth_controller_test.exs
Normal file
54
test/web/controllers/auth_controller_test.exs
Normal file
@ -0,0 +1,54 @@
|
||||
defmodule Mobilizon.Web.AuthControllerTest do
|
||||
use Mobilizon.Web.ConnCase
|
||||
alias Mobilizon.Service.Auth.Authenticator
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@email "someone@somewhere.tld"
|
||||
|
||||
test "login and registration",
|
||||
%{conn: conn} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:ueberauth_auth, %Ueberauth.Auth{
|
||||
strategy: Ueberauth.Strategy.Twitter,
|
||||
extra: %Ueberauth.Auth.Extra{raw_info: %{user: %{"email" => @email}}}
|
||||
})
|
||||
|> get("/auth/twitter/callback")
|
||||
|
||||
assert html_response(conn, 200) =~ "auth-access-token"
|
||||
|
||||
assert %User{confirmed_at: confirmed_at, email: @email} = Authenticator.fetch_user(@email)
|
||||
|
||||
refute is_nil(confirmed_at)
|
||||
end
|
||||
|
||||
test "on bad provider error", %{
|
||||
conn: conn
|
||||
} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:ueberauth_failure, %{errors: [%{message: "Some error"}]})
|
||||
|> get("/auth/nothing")
|
||||
|
||||
assert "/login?code=Login Provider not found&provider=nothing" =
|
||||
redirection = redirected_to(conn, 302)
|
||||
|
||||
conn = get(recycle(conn), redirection)
|
||||
assert html_response(conn, 200)
|
||||
end
|
||||
|
||||
test "on authentication error", %{
|
||||
conn: conn
|
||||
} do
|
||||
conn =
|
||||
conn
|
||||
|> assign(:ueberauth_failure, %{errors: [%{message: "Some error"}]})
|
||||
|> get("/auth/twitter/callback")
|
||||
|
||||
assert "/login?code=Error with Login Provider&provider=twitter" =
|
||||
redirection = redirected_to(conn, 302)
|
||||
|
||||
conn = get(recycle(conn), redirection)
|
||||
assert html_response(conn, 200)
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user