re-implemented login via OIDC
This commit is contained in:
@@ -10,6 +10,8 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("de.srsoftware:tools.mime:1.1.2")
|
||||
implementation("org.json:json:20240303")
|
||||
testImplementation(platform("org.junit:junit-bom:5.10.0"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
implementation("org.xerial:sqlite-jdbc:3.49.0.0")
|
||||
|
||||
@@ -8,7 +8,9 @@ public class Constants {
|
||||
|
||||
public static final String ADDRESS = "address";
|
||||
public static final String ATTACHMENTS = "attachments";
|
||||
public static final String AUTHORIZATION = "Authorization";
|
||||
public static final String BODY = "body";
|
||||
public static final String CONTENT_TYPE = "Content-Type";
|
||||
public static final String DATA = "data";
|
||||
public static final String DATE = "date";
|
||||
public static final String DEFAULT_LANGUAGE = "en";
|
||||
@@ -23,6 +25,8 @@ public class Constants {
|
||||
public static final String ERROR_READ_TABLE = "Failed to read {0} from {1} table";
|
||||
|
||||
public static final String EXPIRATION = "expiration";
|
||||
public static final String GET = "GET";
|
||||
|
||||
public static final String ID = "id";
|
||||
public static final String KEY = "key";
|
||||
public static final String LANGUAGE = "language";
|
||||
@@ -33,6 +37,7 @@ public class Constants {
|
||||
public static final String NUMBER = "number";
|
||||
public static final String OPTIONAL = "optional";
|
||||
public static final String PASSWORD = "password";
|
||||
public static final String POST = "POST";
|
||||
|
||||
public static final String RECEIVERS = "receivers";
|
||||
public static final String REDIRECT = "redirect";
|
||||
|
||||
@@ -3,6 +3,7 @@ package de.srsoftware.umbrella.core;
|
||||
|
||||
public class ResponseCode {
|
||||
public static final int HTTP_UNPROCESSABLE = 422;
|
||||
public static final int HTTP_FAILED_DEPENDENCY = 424;
|
||||
public static final int HTTP_SERVER_ERROR = 500;
|
||||
public static final int HTTP_NOT_IMPLEMENTED = 501;
|
||||
}
|
||||
|
||||
88
core/src/main/java/de/srsoftware/umbrella/core/Util.java
Normal file
88
core/src/main/java/de/srsoftware/umbrella/core/Util.java
Normal file
@@ -0,0 +1,88 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.core;
|
||||
|
||||
import static de.srsoftware.tools.MimeType.MIME_FORM_URL;
|
||||
import static de.srsoftware.tools.MimeType.MIME_JSON;
|
||||
import static de.srsoftware.umbrella.core.Constants.*;
|
||||
import static java.lang.System.Logger.Level.*;
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import de.srsoftware.tools.Query;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class Util {
|
||||
public static final System.Logger LOG = System.getLogger("Util");
|
||||
private Util(){}
|
||||
|
||||
public static HttpURLConnection open(URL url) throws IOException {
|
||||
var conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestProperty("Accept","*/*");
|
||||
conn.setRequestProperty("Host",url.getHost());
|
||||
conn.setRequestProperty("User-Agent","Umbrella/0.1");
|
||||
return conn;
|
||||
}
|
||||
|
||||
public static Object request(String location, Map<String,?> data, String postMime, String auth) throws UmbrellaException {
|
||||
URL url;
|
||||
try {
|
||||
url = new URI(location).toURL();
|
||||
} catch (Exception e) {
|
||||
LOG.log(WARNING,"{0} is not a valid url",location,e);
|
||||
throw new UmbrellaException(500,"{0} is not a valid url",location).causedBy(e);
|
||||
}
|
||||
return request(url,data,postMime,auth);
|
||||
}
|
||||
|
||||
public static Object request(URL target, Map<String,?> data, String postMime, String auth) throws UmbrellaException {
|
||||
String query = null;
|
||||
if (data != null) {
|
||||
query = switch (postMime){
|
||||
case MIME_FORM_URL -> Query.encode(data).orElse(null);
|
||||
case null, default -> {
|
||||
postMime = MIME_JSON;
|
||||
yield new JSONObject(data).toString();
|
||||
}
|
||||
};
|
||||
}
|
||||
var method = query == null ? GET : POST;
|
||||
try {
|
||||
LOG.log(DEBUG,"sending {0} request ({1}) to {2}",method,postMime == null ? "empty" : postMime,target);
|
||||
LOG.log(TRACE,"postData = {0}",query);
|
||||
var conn = open(target);
|
||||
conn.setRequestMethod(method);
|
||||
conn.setRequestProperty(CONTENT_TYPE, postMime);
|
||||
if (auth != null) conn.setRequestProperty(AUTHORIZATION,auth);
|
||||
if (query != null) {
|
||||
conn.setDoOutput(true);
|
||||
var out = conn.getOutputStream();
|
||||
out.write(query.getBytes(UTF_8));
|
||||
out.flush();
|
||||
out.close();
|
||||
}
|
||||
var bos = new ByteArrayOutputStream();
|
||||
if (conn.getResponseCode() == 200) {
|
||||
var is = conn.getInputStream();
|
||||
is.transferTo(bos);
|
||||
is.close();
|
||||
var content = bos.toString(UTF_8);
|
||||
if (content.startsWith("{")) return new JSONObject(content);
|
||||
return content;
|
||||
} else {
|
||||
var is = conn.getErrorStream();
|
||||
is.transferTo(bos);
|
||||
is.close();
|
||||
throw new UmbrellaException(500,bos.toString(UTF_8));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.log(WARNING,"Request to {0} failed: {1}",target,e.getMessage());
|
||||
throw new UmbrellaException(500,"Request to {0} failed!",target).causedBy(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { loadTranslation } from './translations.svelte.js';
|
||||
import { user } from './user.svelte.js';
|
||||
import { Router, Route } from 'svelte-tiny-router';
|
||||
import Callback from "./routes/user/OidcCallback.svelte";
|
||||
import EditService from "./routes/user/EditService.svelte";
|
||||
import Footer from "./Components/Footer.svelte";
|
||||
import Login from "./Components/Login.svelte";
|
||||
@@ -26,9 +27,9 @@
|
||||
</script>
|
||||
|
||||
{#if translations_ready }
|
||||
<Router>
|
||||
{#if user.name }
|
||||
<!-- https://github.com/notnotsamuel/svelte-tiny-router -->
|
||||
<Router>
|
||||
<Menu />
|
||||
<Route path="/user" component={User} />
|
||||
<Route path="/user/:user_id/edit" component={UserEdit} />
|
||||
@@ -36,10 +37,11 @@
|
||||
<Route>
|
||||
<p>Page not found</p>
|
||||
</Route>
|
||||
</Router>
|
||||
{:else}
|
||||
<Login />
|
||||
<Route path="/oidc_callback" component={Callback} />
|
||||
{/if}
|
||||
</Router>
|
||||
<Footer />
|
||||
{:else}
|
||||
<p>Loading translations...</p>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<tbody>
|
||||
{#each connections as connection,i}
|
||||
<tr>
|
||||
<td>{connection.service}</td>
|
||||
<td>{connection.service_id}</td>
|
||||
<td>{connection.foreign_id}</td>
|
||||
<td>
|
||||
<button>{t('user.unlink')}</button>
|
||||
|
||||
33
frontend/src/routes/user/OidcCallback.svelte
Normal file
33
frontend/src/routes/user/OidcCallback.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from '../../translations.svelte.js';
|
||||
import { useTinyRouter } from 'svelte-tiny-router';
|
||||
import { checkUser } from '../../user.svelte.js';
|
||||
|
||||
const router = useTinyRouter();
|
||||
|
||||
let message = $state(t('user.processing_code'));
|
||||
onMount(async () => {
|
||||
let params = new URLSearchParams(location.search);
|
||||
|
||||
if (params.get('code')){
|
||||
const url = `${location.protocol}//${location.host.replace('5173','8080')}/api/user/oidc/token`;
|
||||
const resp = await fetch(url,{
|
||||
method : 'POST',
|
||||
body: JSON.stringify(Object.fromEntries(params)),
|
||||
credentials: 'include'
|
||||
});
|
||||
if (resp.ok){
|
||||
let json = await resp.json();
|
||||
const redirect = json.redirect ? json.redirect : '/user';
|
||||
checkUser();
|
||||
router.navigate(redirect);
|
||||
} else {
|
||||
message = await resp.text();
|
||||
if (!message) message = t(resp);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{message}
|
||||
@@ -19,6 +19,7 @@
|
||||
},
|
||||
"status" : {
|
||||
"403": "Zugriff verweigert",
|
||||
"404": "Seite nicht gefunden",
|
||||
"501": "Nicht implementiert"
|
||||
},
|
||||
"user" : {
|
||||
|
||||
@@ -6,6 +6,7 @@ dependencies{
|
||||
implementation("de.srsoftware:tools.mime:1.1.2")
|
||||
implementation("de.srsoftware:tools.optionals:1.0.0")
|
||||
implementation("de.srsoftware:tools.util:2.0.3")
|
||||
implementation("org.bitbucket.b_c:jose4j:0.9.6")
|
||||
implementation("org.json:json:20240303")
|
||||
implementation("org.xerial:sqlite-jdbc:3.49.0.0")
|
||||
}
|
||||
@@ -1,18 +1,21 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.user;
|
||||
|
||||
import static de.srsoftware.tools.MimeType.MIME_FORM_URL;
|
||||
import static de.srsoftware.tools.Optionals.*;
|
||||
import static de.srsoftware.umbrella.core.Constants.*;
|
||||
import static de.srsoftware.umbrella.core.Paths.LIST;
|
||||
import static de.srsoftware.umbrella.core.Paths.LOGOUT;
|
||||
import static de.srsoftware.umbrella.core.ResponseCode.*;
|
||||
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_SERVER_ERROR;
|
||||
import static de.srsoftware.umbrella.core.Util.open;
|
||||
import static de.srsoftware.umbrella.core.Util.request;
|
||||
import static de.srsoftware.umbrella.user.Constants.*;
|
||||
import static de.srsoftware.umbrella.user.Paths.*;
|
||||
import static de.srsoftware.umbrella.user.Paths.IMPERSONATE;
|
||||
import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION;
|
||||
import static de.srsoftware.umbrella.user.model.DbUser.PERMISSION.*;
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
import static java.lang.System.Logger.Level.*;
|
||||
import static java.net.HttpURLConnection.*;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.text.MessageFormat.format;
|
||||
@@ -26,14 +29,18 @@ import de.srsoftware.umbrella.core.UmbrellaException;
|
||||
import de.srsoftware.umbrella.user.api.LoginServiceDb;
|
||||
import de.srsoftware.umbrella.user.api.UserDb;
|
||||
import de.srsoftware.umbrella.user.model.*;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
import org.jose4j.jwk.HttpsJwks;
|
||||
import org.jose4j.jwt.MalformedClaimException;
|
||||
import org.jose4j.jwt.consumer.InvalidJwtException;
|
||||
import org.jose4j.jwt.consumer.JwtConsumer;
|
||||
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
|
||||
import org.jose4j.keys.resolvers.HttpsJwksVerificationKeyResolver;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
||||
@@ -93,7 +100,7 @@ public class UserModule extends PathHandler {
|
||||
switch (head) {
|
||||
case LIST: return getUserList(ex, user);
|
||||
case LOGOUT: return logout(ex, sessionToken);
|
||||
case OIDC: return getService(ex,user,path);
|
||||
case OIDC: return getOIDC(ex,user,path);
|
||||
case WHOAMI: return getUser(ex, user);
|
||||
|
||||
};
|
||||
@@ -177,13 +184,88 @@ public class UserModule extends PathHandler {
|
||||
head = path.pop();
|
||||
} catch (NumberFormatException ignored) {}
|
||||
switch (head){
|
||||
case OIDC: return postOIDC(ex,path);
|
||||
case IMPERSONATE: return impersonate(ex,targetId);
|
||||
case LOGIN: return postLogin(ex);
|
||||
}
|
||||
return super.doPost(path, ex);
|
||||
}
|
||||
|
||||
private boolean getService(HttpExchange ex, UmbrellaUser user, Path path) throws IOException {
|
||||
private boolean exchangeToken(HttpExchange ex) throws IOException {
|
||||
JSONObject params;
|
||||
try {
|
||||
params = json(ex);
|
||||
} catch (Exception e) {
|
||||
LOG.log(WARNING, "Request does not seem to contain JSON data!", e);
|
||||
return sendContent(ex,HTTP_BAD_REQUEST,"Request does not seem to contain JSON data!");
|
||||
}
|
||||
if (!params.has(CODE)) return sendContent(ex,HTTP_BAD_REQUEST,"missing auth code");
|
||||
if (!params.has(STATE)) return sendContent(ex,HTTP_BAD_REQUEST,"no state submitted");
|
||||
var code = params.getString(CODE);
|
||||
var state = stateMep.remove(params.getString(STATE));
|
||||
if (state == null) return sendContent(ex,HTTP_BAD_REQUEST,"no state submitted");
|
||||
var redirect = url(ex).replaceAll("/api/.*","");
|
||||
var location = state.config.getString(TOKEN_ENDPOINT);
|
||||
|
||||
try {
|
||||
var data = Map.of(GRANT_TYPE,AUTHORIZATION_CODE, CODE,code,REDIRECT_URI,redirect);
|
||||
var resp = request(location,data,MIME_FORM_URL,state.loginService.basicAuth());
|
||||
if (!(resp instanceof JSONObject json)) return sendContent(ex,HTTP_BAD_REQUEST,format("{0} did not return JSON!",location));
|
||||
if (!json.has(ID_TOKEN)) return sendContent(ex,HTTP_FAILED_DEPENDENCY,"Missing ID token – token exchange failed!");
|
||||
var idToken = json.getString(ID_TOKEN);
|
||||
Optional<Token> optToken = SessionToken.from(ex).map(Token::of);
|
||||
Optional<UmbrellaUser> optUser = Optional.empty();
|
||||
|
||||
if (optToken.isPresent()){
|
||||
Session session = users.load(optToken.get());
|
||||
optUser = Optional.of(users.load(session));
|
||||
}
|
||||
var oidcUserId = verifyAndGetUserId(idToken, state);
|
||||
if (optUser.isPresent()){ // user already logged in – this is the case when the new id gets assigned
|
||||
var currentUser = optUser.get();
|
||||
var assignment = new ForeignLogin(state.loginService.name(),oidcUserId,currentUser.id());
|
||||
logins.save(assignment);
|
||||
}
|
||||
var user = users.load(logins.getUserId(state.loginService.name(), oidcUserId));
|
||||
var session = users.getSession(user);
|
||||
var returnTo = "/user";
|
||||
if (state.config.has("returnTo")) returnTo = state.config.getString("returnTo");
|
||||
session.cookie().addTo(ex);
|
||||
return sendContent(ex,Map.of(REDIRECT,returnTo));
|
||||
} catch (UmbrellaException e){
|
||||
return send(ex,e);
|
||||
}
|
||||
}
|
||||
|
||||
private String verifyAndGetUserId(String jwt, State state) throws UmbrellaException {
|
||||
var jwksEndpoint = state.config.getString(JWKS_ENDPOINT);
|
||||
var audience = state.loginService.clientId();
|
||||
var httpJwks = new HttpsJwks(jwksEndpoint);
|
||||
var resolver = new HttpsJwksVerificationKeyResolver(httpJwks);
|
||||
JwtConsumer consumer = new JwtConsumerBuilder()
|
||||
.setVerificationKeyResolver(resolver)
|
||||
.setExpectedAudience(audience)
|
||||
.build();
|
||||
try {
|
||||
var claims = consumer.processToClaims(jwt);
|
||||
return claims.getSubject();
|
||||
} catch (InvalidJwtException | MalformedClaimException e) {
|
||||
throw new UmbrellaException(500,"Failed to verify JWT!").causedBy(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean getConnectedServices(HttpExchange ex, UmbrellaUser user) throws IOException {
|
||||
if (user == null) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex);
|
||||
try {
|
||||
var connections = logins.listAssignments(user.id()).stream().map(ForeignLogin::toMap);
|
||||
return sendContent(ex,connections);
|
||||
} catch (UmbrellaException e) {
|
||||
return sendContent(ex,e.statusCode(),e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean getOIDC(HttpExchange ex, UmbrellaUser user, Path path) throws IOException {
|
||||
var head = path.pop();
|
||||
return switch (head){
|
||||
case BUTTONS -> getOidcButtons(ex);
|
||||
@@ -191,16 +273,19 @@ public class UserModule extends PathHandler {
|
||||
case CONNECTED -> getConnectedServices(ex,user);
|
||||
case REDIRECT -> getOidcRedirect(ex,path.pop());
|
||||
case null -> super.doGet(path,ex);
|
||||
default -> getService(ex,user,head);
|
||||
default -> getOIDC(ex,user,head);
|
||||
};
|
||||
}
|
||||
|
||||
public static HttpURLConnection open(URL url) throws IOException {
|
||||
var conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setRequestProperty("Accept","*/*");
|
||||
conn.setRequestProperty("Host",url.getHost());
|
||||
conn.setRequestProperty("User-Agent","Umbrella/0.1");
|
||||
return conn;
|
||||
private boolean getOIDC(HttpExchange ex, UmbrellaUser user, String serviceId) throws IOException {
|
||||
if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(MANAGE_LOGIN_SERVICES))) return sendEmptyResponse(HTTP_FORBIDDEN,ex);
|
||||
try {
|
||||
return sendContent(ex,logins.loadLoginService(serviceId).toMap());
|
||||
} catch (UmbrellaException e) {
|
||||
return sendContent(ex,e.statusCode(),e.getMessage());
|
||||
} catch (IOException e) {
|
||||
return sendContent(ex,HTTP_SERVER_ERROR,e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject getOidcConfig(LoginService service) throws UmbrellaException {
|
||||
@@ -230,7 +315,7 @@ public class UserModule extends PathHandler {
|
||||
var returnTo = queryParam(ex).get("returnTo");
|
||||
if (isSet(returnTo)) config.put("returnTo",returnTo);
|
||||
var url = url(ex);
|
||||
var callback = url.replaceAll("/api/.*", "/callback"); // TODO: frontendPath an zweiter stelle
|
||||
var callback = url.replaceAll("/api/.*", "/oidc_callback"); // TODO: frontendPath an zweiter stelle
|
||||
var authEndpoint = config.getString(AUTH_ENDPOINT);
|
||||
var clientId = loginService.clientId();
|
||||
var state = UUID.randomUUID().toString();
|
||||
@@ -241,32 +326,6 @@ public class UserModule extends PathHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean send(HttpExchange ex, UmbrellaException e) throws IOException {
|
||||
return sendContent(ex,e.statusCode(),e.getMessage());
|
||||
}
|
||||
|
||||
private boolean getConnectedServices(HttpExchange ex, UmbrellaUser user) throws IOException {
|
||||
if (user == null) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex);
|
||||
try {
|
||||
var connections = logins.listAssignments(user.id()).stream().map(ForeignLogin::toMap);
|
||||
return sendContent(ex,connections);
|
||||
} catch (UmbrellaException e) {
|
||||
return sendContent(ex,e.statusCode(),e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean getService(HttpExchange ex, UmbrellaUser user, String serviceId) throws IOException {
|
||||
if (!(user instanceof DbUser dbUser && dbUser.permissions().contains(MANAGE_LOGIN_SERVICES))) return sendEmptyResponse(HTTP_FORBIDDEN,ex);
|
||||
try {
|
||||
return sendContent(ex,logins.loadLoginService(serviceId).toMap());
|
||||
} catch (UmbrellaException e) {
|
||||
return sendContent(ex,e.statusCode(),e.getMessage());
|
||||
} catch (IOException e) {
|
||||
return sendContent(ex,HTTP_SERVER_ERROR,e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean getOidcButtons(HttpExchange ex) throws IOException {
|
||||
try {
|
||||
var services = logins.listLoginServices().stream().map(LoginService::name);
|
||||
@@ -334,6 +393,14 @@ public class UserModule extends PathHandler {
|
||||
return sendEmptyResponse(HTTP_UNAUTHORIZED,ex);
|
||||
}
|
||||
|
||||
|
||||
private boolean postOIDC(HttpExchange ex, Path path) throws IOException {
|
||||
return switch (path.pop()){
|
||||
case TOKEN -> exchangeToken(ex);
|
||||
case null, default -> super.doPost(path,ex);
|
||||
};
|
||||
}
|
||||
|
||||
private boolean patchPassword(HttpExchange ex, UmbrellaUser requestingUser) throws IOException {
|
||||
if (!(requestingUser instanceof DbUser user)) return sendContent(ex, HTTP_SERVER_ERROR,"DbUser expected");
|
||||
JSONObject json;
|
||||
@@ -387,6 +454,8 @@ public class UserModule extends PathHandler {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
static int score(String password){
|
||||
if (password == null) return 0;
|
||||
var score = 0;
|
||||
@@ -397,6 +466,10 @@ public class UserModule extends PathHandler {
|
||||
return score;
|
||||
}
|
||||
|
||||
private boolean send(HttpExchange ex, UmbrellaException e) throws IOException {
|
||||
return sendContent(ex,e.statusCode(),e.getMessage());
|
||||
}
|
||||
|
||||
private boolean update(HttpExchange ex, DbUser user, JSONObject json) throws UmbrellaException, IOException {
|
||||
var id = user.id();
|
||||
var name = json.has(NAME) && json.get(NAME) instanceof String s && !s.isBlank() ? s : user.name();
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
package de.srsoftware.umbrella.user.model;
|
||||
|
||||
import static de.srsoftware.umbrella.core.Constants.USER_ID;
|
||||
import static de.srsoftware.umbrella.user.Constants.FOREIGN_ID;
|
||||
import static de.srsoftware.umbrella.user.Constants.OIDC;
|
||||
import static de.srsoftware.umbrella.user.Constants.*;
|
||||
|
||||
import de.srsoftware.tools.Mappable;
|
||||
import java.util.Map;
|
||||
@@ -15,6 +14,6 @@ public record ForeignLogin(String loginService, String foreingId, Long userId) i
|
||||
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
return Map.of(OIDC,loginService,FOREIGN_ID,foreingId, USER_ID,userId);
|
||||
return Map.of(SERVICE_ID,loginService,FOREIGN_ID,foreingId, USER_ID,userId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user