implement logout

Signed-off-by: MiniDigger <admin@benndorf.dev>
This commit is contained in:
MiniDigger 2021-11-01 11:06:08 +01:00
parent 6b3cd04384
commit 364f8ecb8b
11 changed files with 197 additions and 11 deletions

View File

@ -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'"],

View File

@ -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);
}
});
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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()

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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"

View 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
)