#authentication #single-page-application #quarkus-oidc
Вопрос:
В настоящее время я работаю над личным проектом, который включает в себя API REST Quarkus в качестве бэкэнда, Keycloak в качестве поставщика OpenID Connect и приложение Vue в качестве интерфейса. Я просто не могу понять, как заставить эти три компонента хорошо сочетаться для аутентификации пользователей при сохранении надлежащей безопасности.
Согласно проекту v8 OAuth 2.0 для браузерных приложений, SPA не должен сохранять токены доступа и (возможно) обновления, поскольку в таком сценарии их трудно безопасно хранить. Это означает, что серверная часть должна выступать в качестве Проверяющей стороны, инициируя поток кода авторизации OIDC. Затем я бы либо сохранил файл cookie сеанса в своем SPA (чего я бы предпочел не делать, чтобы сохранить API без состояния), либо сохранил токены внутри защищенного файла cookie HttpOnly на том же сайте. Этот подход-то, чего я пытаюсь достичь, но пока без особого успеха.
Реализация прототипа
Мое приложение Quarkus использует quarkus-oicd
расширение. Насколько я понимаю, я должен добавить следующую конфигурацию в Quarkus’ application.properties
:
quarkus.oidc.application-type=web-app
quarkus.oidc.client-id=myClientId
quarkus.oidc.credentials.secret=********
quarkus.oidc.auth-server-url=http://127.0.0.1:8082/auth/realms/myRealm
Существо application-type=web-app
, которое говорит Кварку, что оно отвечает за инициирование потока кода авторизации. Альтернативой может быть service
то , что в этом случае Quarkus проверяет только токены на предъявителя, которые клиент отправляет в API.
API работает на порту 8081 и предоставляет только один пример ресурса:
@Path("/hello")
@Authenticated
public class ReactiveGreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello RESTEasy Reactive";
}
}
Этот простой компонент Vue предназначен для проверки концепции:
// FetchComponent.vue
<template>
<div>
<button v-on:click="fetchFromBackend">Fetch</button>
<p><b>Output:</b>{{ message }}</p>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
data() {
return {
message: "Click the button to fetch.",
};
},
methods: {
fetchFromBackend(): void {
this.message = "Waiting...";
fetch("http://localhost:8081/hello", {
credentials: "include",
})
.then((resp) => {
console.log(resp);
if (resp.redirected) {
window.location.assign(resp.url);
} else {
return resp.text().then((text) => (this.message = text));
}
})
.catch((reason) => (this.message = "Caught error: " reason));
},
},
});
</script>
<style></style>
Desired outcome
I’d have expected that the call to the back-end without a valid token gets redirected to Keycloak’s authentication page. The user would then enter their credentials, be logged in and redirected back to the SPA with an auth code. It calls the API again. On the back-channel, Quarkus exchanges the auth code against a token and forwards it to the client in form of a cookie. This cookie would be used to authenticate the user for any further API calls.
Actual outcome
When the back-end is called without a valid token, it redirects to Keycloak’s login page, as expected. Apparently, though, there is simply no way to navigate to the redirected URL from JS. The fetch specification states: «Redirects (a response whose status or internal response’s (if any) status is a redirect status) are not exposed to APIs. Exposing redirects might leak information not otherwise available through a cross-site scripting attack.» The redirect: 'manual'
option in a fetch request is somewhat of a red herring. It doesn’t do what one would expect and it certainly doesn’t allow me access to the redirect URL.
What happens instead is that the browser transparently follows the redirect and tries to fetch the login URL. That doesn’t work at all. It results in a CORS error, because Keycloak doesn’t set the relevant headers (and I suppose it shouldn’t, because this isn’t how it’s supposed to work).
I have no clue how to proceed from here but I presume that the answer is extremely obvious to more experienced people.
As a closing remark I’d like to add that this architecture wasn’t the result of a very well-informed decision making process. I chose it mostly because:
- Quarkus: Java is currently my primary language at my job
- Keycloak: I wanted to try my hand at proper externalized IAM and SSO for a while now. This seemed a good opportunity.
- Vue: Я хотел что-то, чтобы тренировать свои навыки JS и что хорошо смотрелось бы в моем резюме. Любой из нынешней серии фреймворков для горячих СПА-салонов подошел бы по всем параметрам.
Таким образом, любые ответы в духе «это ужасная настройка, просто не делайте этого, попробуйте X вместо этого», безусловно, также приветствуются, хотя я все равно хотел бы решить эту головоломку из чувства гордости.