mirror of
https://github.com/HangarMC/Hangar.git
synced 2024-11-21 01:21:54 +08:00
implement logout
Signed-off-by: MiniDigger <admin@benndorf.dev>
This commit is contained in:
parent
6b3cd04384
commit
364f8ecb8b
@ -122,6 +122,7 @@ export default {
|
||||
proxyHost + '/signup',
|
||||
proxyHost + '/login',
|
||||
proxyHost + '/logout',
|
||||
proxyHost + '/handle-logout',
|
||||
proxyHost + '/refresh',
|
||||
proxyHost + '/invalidate',
|
||||
proxyHost + '/v2/api-docs/',
|
||||
@ -132,6 +133,8 @@ export default {
|
||||
proxyHost + '/statusz',
|
||||
// auth
|
||||
lazyAuthHost + '/avatar',
|
||||
lazyAuthHost + '/oauth/logout',
|
||||
oauthHost + '/oauth2',
|
||||
],
|
||||
|
||||
i18n: {
|
||||
@ -202,7 +205,14 @@ export default {
|
||||
],
|
||||
frameSrc: ["'self'", 'http://localhost/', 'https://papermc.io/', 'https://hangar.crowdin.com', 'https://www.youtube-nocookie.com'],
|
||||
manifestSrc: ["'self'"],
|
||||
connectSrc: ["'self'", 'https://www.google-analytics.com', 'https://stats.g.doubleclick.net', 'https://hangar.crowdin.com'],
|
||||
connectSrc: [
|
||||
"'self'",
|
||||
'https://www.google-analytics.com',
|
||||
'https://stats.g.doubleclick.net',
|
||||
'https://hangar.crowdin.com',
|
||||
'http://localhost:3001',
|
||||
'https://hangar-auth.benndorf.dev',
|
||||
],
|
||||
mediaSrc: ["'self'"],
|
||||
objectSrc: ["'none'"],
|
||||
baseUri: ["'none'"],
|
||||
|
@ -14,7 +14,12 @@ const createAuth = ({ app: { $cookies }, $axios, store, $api, redirect }: Contex
|
||||
return this.updateUser(token);
|
||||
}
|
||||
|
||||
async logout(shouldRedirect = true): Promise<void> {
|
||||
async logout() {
|
||||
const token = await $api.getToken(true);
|
||||
location.replace(`/logout?returnUrl=${process.env.publicHost}/logged-out&t=${token}`);
|
||||
}
|
||||
|
||||
async invalidate(shouldRedirect = true) {
|
||||
store.commit('auth/SET_USER', null);
|
||||
store.commit('auth/SET_TOKEN', null);
|
||||
store.commit('auth/SET_AUTHED', false);
|
||||
@ -46,7 +51,7 @@ const createAuth = ({ app: { $cookies }, $axios, store, $api, redirect }: Contex
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
console.log('LOGGING OUT ON updateUser');
|
||||
return this.logout(process.client);
|
||||
return this.invalidate(process.client);
|
||||
});
|
||||
}
|
||||
|
||||
@ -59,7 +64,7 @@ const createAuth = ({ app: { $cookies }, $axios, store, $api, redirect }: Contex
|
||||
return this.processLogin(token);
|
||||
}
|
||||
} else {
|
||||
return this.logout(process.client);
|
||||
return this.invalidate(process.client);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ public class SSOConfig {
|
||||
private String oauthUrl = "http://localhost:4444";
|
||||
private String loginUrl = "/oauth2/auth/";
|
||||
private String tokenUrl = "/oauth2/token";
|
||||
private String logoutUrl = "/oauth2/sessions/logout";
|
||||
private String clientId = "my-client";
|
||||
|
||||
private String authUrl = "http://localhost:3001";
|
||||
@ -40,6 +41,14 @@ public class SSOConfig {
|
||||
this.signupUrl = signupUrl;
|
||||
}
|
||||
|
||||
public String getLogoutUrl() {
|
||||
return logoutUrl;
|
||||
}
|
||||
|
||||
public void setLogoutUrl(String logoutUrl) {
|
||||
this.logoutUrl = logoutUrl;
|
||||
}
|
||||
|
||||
public String getTokenUrl() {
|
||||
return tokenUrl;
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package io.papermc.hangar.controller;
|
||||
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -14,11 +16,13 @@ import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import io.papermc.hangar.HangarComponent;
|
||||
import io.papermc.hangar.db.dao.internal.table.auth.UserOauthTokenDAO;
|
||||
import io.papermc.hangar.exceptions.HangarApiException;
|
||||
import io.papermc.hangar.model.api.auth.RefreshResponse;
|
||||
import io.papermc.hangar.model.db.UserTable;
|
||||
import io.papermc.hangar.model.internal.sso.AuthUser;
|
||||
import io.papermc.hangar.model.internal.sso.URLWithNonce;
|
||||
import io.papermc.hangar.security.annotations.LoggedIn;
|
||||
import io.papermc.hangar.security.configs.SecurityConfig;
|
||||
import io.papermc.hangar.service.AuthenticationService;
|
||||
import io.papermc.hangar.service.TokenService;
|
||||
@ -76,7 +80,7 @@ public class LoginController extends HangarComponent {
|
||||
globalRoleService.removeAllGlobalRoles(user.getId());
|
||||
authUser.getGlobalRoles().forEach(globalRole -> globalRoleService.addRole(globalRole.create(null, user.getId(), true)));
|
||||
tokenService.createTokenForUser(user);
|
||||
return redirectBackOnSuccessfulLogin(url);
|
||||
return addBaseAndRedirect(url);
|
||||
}
|
||||
|
||||
@GetMapping("/refresh")
|
||||
@ -94,6 +98,37 @@ public class LoginController extends HangarComponent {
|
||||
}
|
||||
}
|
||||
|
||||
@LoggedIn
|
||||
@GetMapping(path = "/logout", params = "returnUrl")
|
||||
public RedirectView logout(@RequestParam(defaultValue = "/logged-out") String returnUrl) {
|
||||
if (config.fakeUser.isEnabled()) {
|
||||
response.addCookie(new Cookie("url", returnUrl));
|
||||
return new RedirectView("/handle-logout");
|
||||
} else {
|
||||
response.addCookie(new Cookie("url", returnUrl));
|
||||
return redirectToSso(ssoService.getLogoutUrl(config.getBaseUrl() + "/handle-logout", getHangarPrincipal()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping(path = "/handle-logout", params = "state")
|
||||
public RedirectView loggedOut(@RequestParam String state, @CookieValue(value = "url", defaultValue = "/logged-out") String returnUrl, @CookieValue(name = SecurityConfig.AUTH_NAME_REFRESH_COOKIE, required = false) String refreshToken) {
|
||||
// get username
|
||||
DecodedJWT decodedJWT = tokenService.verify(state);
|
||||
String username = decodedJWT.getSubject();
|
||||
// invalidate id token
|
||||
ssoService.logout(username);
|
||||
// invalidate refresh token
|
||||
if (refreshToken != null) {
|
||||
tokenService.invalidateToken(refreshToken);
|
||||
}
|
||||
// invalidate session
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
session.invalidate();
|
||||
}
|
||||
return addBaseAndRedirect(returnUrl);
|
||||
}
|
||||
|
||||
@GetMapping("/signup")
|
||||
public RedirectView signUp(@RequestParam(defaultValue = "") String returnUrl) {
|
||||
if (config.fakeUser.isEnabled()) {
|
||||
@ -102,7 +137,7 @@ public class LoginController extends HangarComponent {
|
||||
return new RedirectView(ssoService.getSignupUrl(returnUrl));
|
||||
}
|
||||
|
||||
private RedirectView redirectBackOnSuccessfulLogin(String url) {
|
||||
private RedirectView addBaseAndRedirect(String url) {
|
||||
if (!url.startsWith("http")) {
|
||||
if (url.startsWith("/")) {
|
||||
url = config.getBaseUrl() + url;
|
||||
|
@ -0,0 +1,27 @@
|
||||
package io.papermc.hangar.db.dao.internal.table.auth;
|
||||
|
||||
import org.jdbi.v3.sqlobject.config.RegisterConstructorMapper;
|
||||
import org.jdbi.v3.sqlobject.customizer.BindBean;
|
||||
import org.jdbi.v3.sqlobject.customizer.Timestamped;
|
||||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import io.papermc.hangar.model.db.auth.UserOauthTokenTable;
|
||||
|
||||
@Repository
|
||||
@RegisterConstructorMapper(UserOauthTokenTable.class)
|
||||
public interface UserOauthTokenDAO {
|
||||
|
||||
@Timestamped
|
||||
@GetGeneratedKeys
|
||||
@SqlUpdate("INSERT INTO user_oauth_token (created_at, username, id_token) VALUES (:now, :username, :idToken)")
|
||||
UserOauthTokenTable insert(@BindBean UserOauthTokenTable userOauthTokenTable);
|
||||
|
||||
@SqlQuery("SELECT * FROM user_oauth_token WHERE username = :username ORDER BY created_at DESC LIMIT 1")
|
||||
UserOauthTokenTable getByUsername(String username);
|
||||
|
||||
@SqlUpdate("DELETE FROM user_oauth_token WHERE username = :username")
|
||||
void remove(String username);
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package io.papermc.hangar.model.db.auth;
|
||||
|
||||
import org.jdbi.v3.core.mapper.reflect.JdbiConstructor;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import io.papermc.hangar.model.db.Table;
|
||||
|
||||
public class UserOauthTokenTable extends Table {
|
||||
|
||||
private final String username;
|
||||
private final String idToken;
|
||||
|
||||
@JdbiConstructor
|
||||
public UserOauthTokenTable(OffsetDateTime createdAt, long id, String username, String idToken) {
|
||||
super(createdAt, id);
|
||||
this.username = username;
|
||||
this.idToken = idToken;
|
||||
}
|
||||
|
||||
public UserOauthTokenTable(String username, String idToken) {
|
||||
this.username = username;
|
||||
this.idToken = idToken;
|
||||
}
|
||||
|
||||
public String getIdToken() {
|
||||
return idToken;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UserOauthTokenTable{" +
|
||||
"createdAt=" + createdAt +
|
||||
", id=" + id +
|
||||
", username='" + username + '\'' +
|
||||
", idToken='" + idToken + '\'' +
|
||||
"} " + super.toString();
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ import org.springframework.security.web.authentication.AnonymousAuthenticationFi
|
||||
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
@Configuration
|
||||
@ -28,6 +29,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
private static final RequestMatcher API_MATCHER = new AndRequestMatcher(new AntPathRequestMatcher("/api/**"), new NegatedRequestMatcher(new AntPathRequestMatcher("/api/v1/authenticate/**")));
|
||||
private static final RequestMatcher PUBLIC_API_MATCHER = new AndRequestMatcher(new AntPathRequestMatcher("/api/v1/**"), new NegatedRequestMatcher(new AntPathRequestMatcher("/api/v1/authenticate/**")));
|
||||
private static final RequestMatcher INTERNAL_API_MATCHER = new AntPathRequestMatcher("/api/internal/**");
|
||||
private static final RequestMatcher LOGOUT_MATCHER = new AntPathRequestMatcher("/logout");
|
||||
private static final RequestMatcher INVALIDATE_MATCHER = new AntPathRequestMatcher("/invalidate");
|
||||
|
||||
private final TokenService tokenService;
|
||||
private final AuthenticationEntryPoint authenticationEntryPoint;
|
||||
@ -53,7 +56,13 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
.csrf().disable()
|
||||
|
||||
// Custom auth filters
|
||||
.addFilterBefore(new HangarAuthenticationFilter(API_MATCHER, tokenService, authenticationManager(), authenticationEntryPoint), AnonymousAuthenticationFilter.class)
|
||||
.addFilterBefore(new HangarAuthenticationFilter(
|
||||
new OrRequestMatcher(API_MATCHER, LOGOUT_MATCHER, INVALIDATE_MATCHER),
|
||||
tokenService,
|
||||
authenticationManager(),
|
||||
authenticationEntryPoint),
|
||||
AnonymousAuthenticationFilter.class
|
||||
)
|
||||
|
||||
// .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
|
||||
|
||||
|
@ -99,6 +99,14 @@ public class TokenService extends HangarComponent {
|
||||
.sign(getAlgo());
|
||||
}
|
||||
|
||||
public String simple(String username) {
|
||||
return JWT.create()
|
||||
.withIssuer(config.security.getTokenIssuer())
|
||||
.withExpiresAt(new Date(Instant.now().plus(config.security.getTokenExpiry()).toEpochMilli()))
|
||||
.withSubject(username)
|
||||
.sign(getAlgo());
|
||||
}
|
||||
|
||||
public HangarPrincipal parseHangarPrincipal(DecodedJWT decodedJWT) {
|
||||
String subject = decodedJWT.getSubject();
|
||||
Long userId = decodedJWT.getClaim("id").asLong();
|
||||
|
@ -3,8 +3,6 @@ package io.papermc.hangar.service.internal.auth;
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@ -24,12 +22,16 @@ import java.util.Locale;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import io.papermc.hangar.config.hangar.HangarConfig;
|
||||
import io.papermc.hangar.db.dao.internal.table.auth.UserOauthTokenDAO;
|
||||
import io.papermc.hangar.db.dao.internal.table.auth.UserSignOnDAO;
|
||||
import io.papermc.hangar.model.db.auth.UserOauthTokenTable;
|
||||
import io.papermc.hangar.model.db.auth.UserSignOnTable;
|
||||
import io.papermc.hangar.model.internal.sso.AuthUser;
|
||||
import io.papermc.hangar.model.internal.sso.TokenResponce;
|
||||
import io.papermc.hangar.model.internal.sso.Traits;
|
||||
import io.papermc.hangar.model.internal.sso.URLWithNonce;
|
||||
import io.papermc.hangar.security.authentication.HangarPrincipal;
|
||||
import io.papermc.hangar.service.TokenService;
|
||||
|
||||
@Service
|
||||
public class SSOService {
|
||||
@ -37,12 +39,16 @@ public class SSOService {
|
||||
private final HangarConfig hangarConfig;
|
||||
private final UserSignOnDAO userSignOnDAO;
|
||||
private final RestTemplate restTemplate;
|
||||
private final UserOauthTokenDAO userOauthTokenDAO;
|
||||
private final TokenService tokenService;
|
||||
|
||||
@Autowired
|
||||
public SSOService(HangarConfig hangarConfig, UserSignOnDAO userSignOnDAO, RestTemplate restTemplate) {
|
||||
public SSOService(HangarConfig hangarConfig, UserSignOnDAO userSignOnDAO, RestTemplate restTemplate, UserOauthTokenDAO userOauthTokenDAO, TokenService tokenService) {
|
||||
this.hangarConfig = hangarConfig;
|
||||
this.userSignOnDAO = userSignOnDAO;
|
||||
this.restTemplate = restTemplate;
|
||||
this.userOauthTokenDAO = userOauthTokenDAO;
|
||||
this.tokenService = tokenService;
|
||||
}
|
||||
|
||||
private boolean isNonceValid(String nonce) {
|
||||
@ -67,6 +73,22 @@ public class SSOService {
|
||||
return new URLWithNonce(url, generatedNonce);
|
||||
}
|
||||
|
||||
private String idToken(String username) {
|
||||
UserOauthTokenTable token = userOauthTokenDAO.getByUsername(username);
|
||||
return token.getIdToken();
|
||||
}
|
||||
|
||||
public URLWithNonce getLogoutUrl(String returnUrl, HangarPrincipal user) {
|
||||
String idToken = idToken(user.getName());
|
||||
String generatedNonce = nonce();
|
||||
String url = UriComponentsBuilder.fromUriString(hangarConfig.sso.getOauthUrl() + hangarConfig.sso.getLogoutUrl())
|
||||
.queryParam("post_logout_redirect_uri", returnUrl)
|
||||
.queryParam("id_token_hint", idToken)
|
||||
.queryParam("state", tokenService.simple(user.getName()))
|
||||
.build().toUriString();
|
||||
return new URLWithNonce(url, generatedNonce);
|
||||
}
|
||||
|
||||
public String getSignupUrl(String returnUrl) {
|
||||
// TODO figure out what we wanna do here
|
||||
return hangarConfig.sso.getAuthUrl() + hangarConfig.sso.getSignupUrl();
|
||||
@ -97,6 +119,7 @@ public class SSOService {
|
||||
if (!isNonceValid(nonce)) {
|
||||
return null;
|
||||
}
|
||||
saveIdToken(token, traits.getUsername());
|
||||
return authUser;
|
||||
}
|
||||
|
||||
@ -120,4 +143,12 @@ public class SSOService {
|
||||
public UserSignOnTable insert(String nonce) {
|
||||
return userSignOnDAO.insert(new UserSignOnTable(nonce));
|
||||
}
|
||||
|
||||
public void saveIdToken(String idToken, String username) {
|
||||
userOauthTokenDAO.insert(new UserOauthTokenTable(username, idToken));
|
||||
}
|
||||
|
||||
public void logout(String username) {
|
||||
userOauthTokenDAO.remove(username);
|
||||
}
|
||||
}
|
||||
|
@ -121,9 +121,10 @@ hangar:
|
||||
|
||||
sso:
|
||||
enabled: true
|
||||
oauth-url: "http://localhost:4444"
|
||||
oauth-url: "http://localhost:3000" # proxied to 4444
|
||||
login-url: "/oauth2/auth/"
|
||||
token-url: "/oauth2/token"
|
||||
logout-url: "/oauth2/sessions/logout"
|
||||
client-id: "my-client"
|
||||
|
||||
auth-url: "http://localhost:3001"
|
||||
|
8
src/main/resources/db/migration/V1.6.0__saveIdToken.sql
Normal file
8
src/main/resources/db/migration/V1.6.0__saveIdToken.sql
Normal file
@ -0,0 +1,8 @@
|
||||
CREATE TABLE user_oauth_token(
|
||||
id bigserial NOT NULL
|
||||
CONSTRAINT user_oauth_token_pkey
|
||||
PRIMARY KEY,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
username varchar(255),
|
||||
id_token text
|
||||
)
|
Loading…
Reference in New Issue
Block a user