mirror of
https://github.com/HangarMC/Hangar.git
synced 2025-02-17 15:01:42 +08:00
feat: sudo for accounts without passwords
This commit is contained in:
parent
bf1f824e39
commit
89386deeac
@ -20,9 +20,13 @@ import io.papermc.hangar.components.auth.service.WebAuthNService;
|
||||
import io.papermc.hangar.model.db.UserTable;
|
||||
import io.papermc.hangar.security.annotations.Anyone;
|
||||
import io.papermc.hangar.security.annotations.ratelimit.RateLimit;
|
||||
import io.papermc.hangar.security.annotations.unlocked.Unlocked;
|
||||
import io.papermc.hangar.security.authentication.HangarPrincipal;
|
||||
import io.papermc.hangar.service.internal.users.UserService;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
@ -52,6 +56,13 @@ public class LoginController extends HangarComponent {
|
||||
this.authService = authService;
|
||||
}
|
||||
|
||||
@Unlocked
|
||||
@PostMapping("login/sudo")
|
||||
public LoginResponse loginSudo() {
|
||||
final List<CredentialType> types = this.credentialsService.getCredentialTypes(this.getHangarPrincipal().getUserId());
|
||||
return new LoginResponse(this.getHangarPrincipal().getAal(), types, null);
|
||||
}
|
||||
|
||||
@Anyone
|
||||
@PostMapping("login/password")
|
||||
public LoginResponse loginPassword(@RequestBody final LoginPasswordForm form) {
|
||||
@ -68,7 +79,14 @@ public class LoginController extends HangarComponent {
|
||||
@Anyone
|
||||
@PostMapping("login/webauthn")
|
||||
public LoginResponse loginWebAuthN(@RequestBody final LoginWebAuthNForm form) throws IOException {
|
||||
final UserTable userTable = this.verifyPassword(form.usernameOrEmail(), form.password());
|
||||
final Optional<HangarPrincipal> principal = this.getOptionalHangarPrincipal();
|
||||
final UserTable userTable;
|
||||
//noinspection OptionalIsPresent
|
||||
if (principal.isPresent()) {
|
||||
userTable = Objects.requireNonNull(this.userService.getUserTable(principal.get().getUserId()));
|
||||
} else {
|
||||
userTable = this.verifyPassword(form.usernameOrEmail(), form.password());
|
||||
}
|
||||
|
||||
final var pkc = PublicKeyCredential.parseAssertionResponseJson(form.publicKeyCredentialJson());
|
||||
|
||||
@ -93,6 +111,12 @@ public class LoginController extends HangarComponent {
|
||||
@Anyone
|
||||
@PostMapping("login/totp")
|
||||
public LoginResponse loginTotp(@RequestBody final LoginTotpForm form) {
|
||||
final Optional<HangarPrincipal> principal = this.getOptionalHangarPrincipal();
|
||||
if (principal.isPresent()) {
|
||||
this.credentialsService.verifyTotp(principal.get().getUserId(), form.totpCode());
|
||||
return this.authService.setAalAndLogin(Objects.requireNonNull(this.userService.getUserTable(principal.get().getUserId())), 2);
|
||||
}
|
||||
|
||||
final UserTable userTable = this.verifyPassword(form.usernameOrEmail(), form.password());
|
||||
this.credentialsService.verifyTotp(userTable.getUserId(), form.totpCode());
|
||||
return this.authService.setAalAndLogin(userTable, 2);
|
||||
@ -101,6 +125,12 @@ public class LoginController extends HangarComponent {
|
||||
@Anyone
|
||||
@PostMapping("login/backup")
|
||||
public LoginResponse loginBackup(@RequestBody final LoginBackupForm form) {
|
||||
final Optional<HangarPrincipal> principal = this.getOptionalHangarPrincipal();
|
||||
if (principal.isPresent()) {
|
||||
this.credentialsService.verifyBackupCode(principal.get().getUserId(), form.backupCode());
|
||||
return this.authService.setAalAndLogin(Objects.requireNonNull(this.userService.getUserTable(principal.get().getUserId())), 2);
|
||||
}
|
||||
|
||||
final UserTable userTable = this.verifyPassword(form.usernameOrEmail(), form.password());
|
||||
this.credentialsService.verifyBackupCode(userTable.getUserId(), form.backupCode());
|
||||
return this.authService.setAalAndLogin(userTable, 2);
|
||||
|
@ -9,6 +9,7 @@ import io.papermc.hangar.components.auth.model.credential.Credential;
|
||||
import io.papermc.hangar.components.auth.model.credential.CredentialType;
|
||||
import io.papermc.hangar.components.auth.model.credential.PasswordCredential;
|
||||
import io.papermc.hangar.components.auth.model.credential.TotpCredential;
|
||||
import io.papermc.hangar.components.auth.model.credential.WebAuthNCredential;
|
||||
import io.papermc.hangar.components.auth.model.db.UserCredentialTable;
|
||||
import io.papermc.hangar.db.customtypes.JSONB;
|
||||
import io.papermc.hangar.model.db.UserTable;
|
||||
@ -142,4 +143,20 @@ public class CredentialsService extends HangarComponent {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isOAuthOnly(final long userId) {
|
||||
// no pw, no totp
|
||||
if (this.getCredential(userId, CredentialType.PASSWORD) == null &&
|
||||
this.getCredential(userId, CredentialType.TOTP) == null) {
|
||||
final UserCredentialTable credential = this.getCredential(userId, CredentialType.WEBAUTHN);
|
||||
// either no webauthn
|
||||
if(credential == null) {
|
||||
return true;
|
||||
}
|
||||
// or not correctly setup webauthn
|
||||
final WebAuthNCredential webAuthNCredential = credential.getCredential().get(WebAuthNCredential.class);
|
||||
return webAuthNCredential == null || webAuthNCredential.credentials().isEmpty();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import io.papermc.hangar.HangarComponent;
|
||||
import io.papermc.hangar.components.auth.dao.UserRefreshTokenDAO;
|
||||
import io.papermc.hangar.components.auth.model.credential.CredentialType;
|
||||
import io.papermc.hangar.components.auth.model.db.UserRefreshToken;
|
||||
import io.papermc.hangar.db.dao.internal.table.auth.ApiKeyDAO;
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
@ -98,8 +99,12 @@ public class TokenService extends HangarComponent {
|
||||
}
|
||||
// in any case, refreshing the cookie is good
|
||||
this.addCookie(SecurityConfig.REFRESH_COOKIE_NAME, userRefreshToken.getToken().toString(), this.config.security.refreshTokenExpiry().toSeconds(), true, this.response);
|
||||
|
||||
// oauth only users are always privileged
|
||||
final boolean privileged = this.credentialsService.isOAuthOnly(userTable.getUserId());
|
||||
|
||||
// then issue a new access token
|
||||
return new RefreshResponse(this.newToken0(userTable, false), userTable);
|
||||
return new RefreshResponse(this.newToken0(userTable, privileged), userTable);
|
||||
}
|
||||
|
||||
public void invalidateToken(final String refreshToken) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { useHead } from "@unhead/vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { ref } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
import * as webauthnJson from "@github/webauthn-json";
|
||||
import { useVuelidate } from "@vuelidate/core";
|
||||
import { useI18n } from "vue-i18n";
|
||||
@ -28,7 +28,9 @@ const i18n = useI18n();
|
||||
const backendData = useBackendData;
|
||||
|
||||
const loading = ref(false);
|
||||
const supportedMethods = ref([]);
|
||||
const supportedMethods = ref<string[]>([]);
|
||||
|
||||
const returnUrl = computed(() => (route.query.returnUrl as string) || "/auth/settings/profile");
|
||||
|
||||
// aal1
|
||||
const username = ref("");
|
||||
@ -37,6 +39,12 @@ const password = ref("");
|
||||
const privileged = (route.query.privileged as boolean) || false;
|
||||
if (privileged) {
|
||||
username.value = useAuthStore().user?.name || "";
|
||||
loading.value = true;
|
||||
const response = await useInternalApi<LoginResponse>("auth/login/sudo", "POST");
|
||||
if (response.types?.length) {
|
||||
supportedMethods.value.push(...response.types);
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function loginPassword() {
|
||||
@ -47,7 +55,7 @@ async function loginPassword() {
|
||||
usernameOrEmail: username.value,
|
||||
password: password.value,
|
||||
});
|
||||
if (response.types?.length > 0) {
|
||||
if (response.types?.length) {
|
||||
supportedMethods.value.push(...response.types);
|
||||
} else {
|
||||
await finish(response);
|
||||
@ -123,9 +131,7 @@ async function finish(response: LoginResponse) {
|
||||
authStore.authenticated = true;
|
||||
authStore.invalidated = false;
|
||||
authStore.token = response.user.accessToken;
|
||||
authStore.privileged = true;
|
||||
const returnUrl = (route.query.returnUrl as string) || "/auth/settings/profile";
|
||||
await router.push(returnUrl);
|
||||
await router.push(returnUrl.value);
|
||||
} else {
|
||||
notification.error("Did not receive user?");
|
||||
}
|
||||
@ -153,18 +159,20 @@ useHead(useSeo("Login", null, route, null));
|
||||
<InputPassword v-model="password" label="Password" name="password" autocomplete="current-password" :rules="[required()]" />
|
||||
<div class="flex gap-2">
|
||||
<Button :disabled="loading" @click.prevent="loginPassword">Login</Button>
|
||||
<Button
|
||||
v-for="provider in backendData.security.oauthProviders"
|
||||
:key="provider"
|
||||
:disabled="loading"
|
||||
:href="'/api/internal/oauth/' + provider + '/login?mode=login&returnUrl=/'"
|
||||
>
|
||||
<template v-if="provider === 'github'">
|
||||
<IconMdiGitHub class="mr-1" />
|
||||
Login with GitHub
|
||||
</template>
|
||||
<template v-else> Login with {{ provider }} </template>
|
||||
</Button>
|
||||
<template v-if="!privileged">
|
||||
<Button
|
||||
v-for="provider in backendData.security.oauthProviders"
|
||||
:key="provider"
|
||||
:disabled="loading"
|
||||
:href="'/api/internal/oauth/' + provider + '/login?mode=login&returnUrl=' + returnUrl"
|
||||
>
|
||||
<template v-if="provider === 'github'">
|
||||
<IconMdiGitHub class="mr-1" />
|
||||
Login with GitHub
|
||||
</template>
|
||||
<template v-else> Login with {{ provider }} </template>
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
<Link v-if="!privileged" button-type="secondary" to="/auth/signup" class="w-max">Don't have an account yet? Create one!</Link>
|
||||
<Link v-if="!privileged" to="/auth/reset" class="w-max">Forgot your password?</Link>
|
||||
|
@ -12,7 +12,6 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
const routePermissionsUser = ref<string | null>(null);
|
||||
const routePermissionsProject = ref<string | null>(null);
|
||||
const invalidated = ref<boolean>(false);
|
||||
const privileged = ref<boolean>(false);
|
||||
|
||||
authLog("create authStore");
|
||||
|
||||
@ -22,5 +21,5 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
routePermissionsProject.value = routePermsProject;
|
||||
}
|
||||
|
||||
return { token, authenticated, user, routePermissions, invalidated, setRoutePerms, routePermissionsUser, routePermissionsProject, aal, privileged };
|
||||
return { token, authenticated, user, routePermissions, invalidated, setRoutePerms, routePermissionsUser, routePermissionsProject, aal };
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user