-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from penguineer/login
Implement OIDC login via GitHub
- Loading branch information
Showing
13 changed files
with
407 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 8 additions & 2 deletions
10
src/main/java/com/penguineering/gartenplus/GartenplusApplication.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,19 @@ | ||
package com.penguineering.gartenplus; | ||
|
||
import com.vaadin.flow.component.page.AppShellConfigurator; | ||
import com.vaadin.flow.component.page.Push; | ||
import org.springframework.boot.SpringApplication; | ||
import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; | ||
import org.springframework.scheduling.annotation.EnableAsync; | ||
|
||
@SpringBootApplication | ||
public class GartenplusApplication { | ||
@EnableJpaRepositories | ||
@Push | ||
@EnableAsync | ||
public class GartenplusApplication implements AppShellConfigurator { | ||
|
||
public static void main(String[] args) { | ||
SpringApplication.run(GartenplusApplication.class, args); | ||
} | ||
|
||
} |
36 changes: 36 additions & 0 deletions
36
src/main/java/com/penguineering/gartenplus/auth/GartenplusUser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package com.penguineering.gartenplus.auth; | ||
|
||
import com.penguineering.gartenplus.auth.user.UserDTO; | ||
import lombok.Getter; | ||
import org.springframework.security.core.GrantedAuthority; | ||
import org.springframework.security.oauth2.core.user.OAuth2User; | ||
|
||
import java.util.Collection; | ||
import java.util.Map; | ||
|
||
public class GartenplusUser implements OAuth2User { | ||
private final OAuth2User origin; | ||
|
||
@Getter | ||
private final UserDTO user; | ||
|
||
public GartenplusUser(OAuth2User origin, UserDTO user) { | ||
this.origin = origin; | ||
this.user = user; | ||
} | ||
|
||
@Override | ||
public Map<String, Object> getAttributes() { | ||
return origin.getAttributes(); | ||
} | ||
|
||
@Override | ||
public Collection<? extends GrantedAuthority> getAuthorities() { | ||
return origin.getAuthorities(); | ||
} | ||
|
||
@Override | ||
public String getName() { | ||
return user.displayName(); | ||
} | ||
} |
85 changes: 85 additions & 0 deletions
85
src/main/java/com/penguineering/gartenplus/auth/GithubOidcUserService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package com.penguineering.gartenplus.auth; | ||
|
||
import com.penguineering.gartenplus.auth.mapping.OidcMapping; | ||
import com.penguineering.gartenplus.auth.mapping.OidcMappingKey; | ||
import com.penguineering.gartenplus.auth.mapping.OidcMappingRepository; | ||
import com.penguineering.gartenplus.auth.user.UserDTO; | ||
import com.penguineering.gartenplus.auth.user.UserEntity; | ||
import com.penguineering.gartenplus.auth.user.UserRepository; | ||
import org.springframework.security.authentication.InternalAuthenticationServiceException; | ||
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; | ||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; | ||
import org.springframework.security.oauth2.core.user.OAuth2User; | ||
import org.springframework.stereotype.Service; | ||
|
||
import java.net.URI; | ||
import java.util.Optional; | ||
|
||
@Service | ||
public class GithubOidcUserService extends DefaultOAuth2UserService { | ||
private final OidcMappingRepository oidcMappingRepository; | ||
private final UserRepository userRepository; | ||
|
||
public GithubOidcUserService( | ||
OidcMappingRepository oidcMappingRepository, | ||
UserRepository userRepository) { | ||
this.oidcMappingRepository = oidcMappingRepository; | ||
this.userRepository = userRepository; | ||
} | ||
|
||
@Override | ||
public OAuth2User loadUser(OAuth2UserRequest userRequest) { | ||
OAuth2User origin = super.loadUser(userRequest); | ||
|
||
try { | ||
// Custom logic to map GitHub user info to your application's user model | ||
return findUserByOriginId(origin) | ||
.map(user -> new GartenplusUser(origin, user)) | ||
.orElseGet(() -> new GartenplusUser(origin, createUser(origin))); | ||
} catch (Exception e) { | ||
throw new InternalAuthenticationServiceException("Problem during login", e); | ||
} | ||
} | ||
|
||
private OidcMappingKey toMappingKey(OAuth2User origin) { | ||
return Optional.ofNullable(origin.getAttribute("id")) | ||
.map(String::valueOf) | ||
.map(id -> OidcMappingKey.of("github", id)) | ||
.orElseThrow(() -> new IllegalArgumentException("id attribute is missing")); | ||
} | ||
|
||
private Optional<UserDTO> findUserByOriginId(OAuth2User origin) { | ||
return Optional | ||
.ofNullable(origin) | ||
.map(this::toMappingKey) | ||
.flatMap(oidcMappingRepository::findById) | ||
.map(OidcMapping::getUserId) | ||
.flatMap(userRepository::findById) | ||
.map(UserEntity::toDTO); | ||
} | ||
|
||
private UserDTO createUser(OAuth2User origin) { | ||
final String origin_id = Optional.ofNullable(origin.getAttribute("id")) | ||
.map(Object::toString) | ||
.orElseThrow(() -> new IllegalArgumentException("id attribute is missing")); | ||
final String origin_name = origin.getAttribute("name"); | ||
final String origin_email = origin.getAttribute("email"); | ||
final String origin_avatar_url = origin.getAttribute("avatar_url"); | ||
|
||
UserEntity userEntity = new UserEntity(); | ||
userEntity.setDisplayName(origin_name); | ||
userEntity.setEmail(origin_email); | ||
Optional.ofNullable(origin_avatar_url) | ||
.map(URI::create) | ||
.ifPresent(userEntity::setAvatarUrl); | ||
UserEntity savedUserEntity = userRepository.save(userEntity); | ||
|
||
OidcMapping oidcMapping = new OidcMapping(); | ||
oidcMapping.setIssuer("github"); | ||
oidcMapping.setOidcId(origin_id); | ||
oidcMapping.setUserId(savedUserEntity.getId()); | ||
oidcMappingRepository.save(oidcMapping); | ||
|
||
return savedUserEntity.toDTO(); | ||
} | ||
} |
34 changes: 34 additions & 0 deletions
34
src/main/java/com/penguineering/gartenplus/auth/SecurityConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package com.penguineering.gartenplus.auth; | ||
|
||
import com.vaadin.flow.spring.security.VaadinWebSecurity; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; | ||
|
||
@Configuration | ||
@EnableWebSecurity | ||
public class SecurityConfig extends VaadinWebSecurity { | ||
@Override | ||
protected void configure(HttpSecurity http) throws Exception { | ||
http.oauth2Login(login -> login | ||
.defaultSuccessUrl("/")); | ||
|
||
http.sessionManagement(session -> session | ||
.sessionFixation().migrateSession()); | ||
|
||
http.logout(logout -> logout | ||
.logoutSuccessUrl("/") | ||
.invalidateHttpSession(true) | ||
.deleteCookies("JSESSIONID")); | ||
|
||
// Allow unauthenticated access to Health Endpoint and logo | ||
http.authorizeHttpRequests(auth -> auth | ||
.requestMatchers(AntPathRequestMatcher.antMatcher("/actuator/health")).permitAll() | ||
.requestMatchers(AntPathRequestMatcher.antMatcher("/assets/GaretnPlus_Logo.png")).permitAll() | ||
); | ||
|
||
super.configure(http); | ||
} | ||
|
||
} |
32 changes: 32 additions & 0 deletions
32
src/main/java/com/penguineering/gartenplus/auth/mapping/OidcMapping.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package com.penguineering.gartenplus.auth.mapping; | ||
|
||
import jakarta.persistence.*; | ||
import lombok.*; | ||
|
||
import java.util.UUID; | ||
|
||
@Entity | ||
@Getter | ||
@Setter | ||
@NoArgsConstructor | ||
@AllArgsConstructor | ||
@EqualsAndHashCode(of = {"issuer", "oidcId"}) | ||
@IdClass(OidcMappingKey.class) | ||
@Table(name = "oidc_mapping") | ||
public class OidcMapping { | ||
@Id | ||
@Column(name = "issuer") | ||
private String issuer; | ||
|
||
@Id | ||
@Column(name = "oidc_id") | ||
private String oidcId; | ||
|
||
@Column(name = "user_id") | ||
private UUID userId; | ||
|
||
@Override | ||
public String toString() { | ||
return issuer + "/" + oidcId + " -> " + userId; | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
src/main/java/com/penguineering/gartenplus/auth/mapping/OidcMappingKey.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package com.penguineering.gartenplus.auth.mapping; | ||
|
||
import lombok.EqualsAndHashCode; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
import lombok.Setter; | ||
import org.springframework.web.server.WebSession; | ||
|
||
import java.io.Serializable; | ||
import java.util.Optional; | ||
|
||
@Getter | ||
@Setter | ||
@NoArgsConstructor | ||
@EqualsAndHashCode | ||
public class OidcMappingKey implements Serializable { | ||
protected static final String SESSION_KEY = "oidc-mapping-key"; | ||
|
||
private String issuer; | ||
private String oidcId; | ||
|
||
public static OidcMappingKey of(String issuer, String oidcId) { | ||
return new OidcMappingKey(issuer, oidcId); | ||
} | ||
|
||
public static Optional<OidcMappingKey> ofWebSession(WebSession session) { | ||
return Optional.ofNullable(session.getAttributes().get(SESSION_KEY)) | ||
.filter(key -> key instanceof OidcMappingKey) | ||
.map(OidcMappingKey.class::cast); | ||
} | ||
|
||
public OidcMappingKey(String issuer, String oidcId) { | ||
this.issuer = issuer; | ||
this.oidcId = oidcId; | ||
} | ||
|
||
public Optional<OidcMappingKey> saveToWebSession(WebSession session) { | ||
return Optional | ||
.ofNullable(session.getAttributes().put(SESSION_KEY, this)) | ||
.filter(key -> key instanceof OidcMappingKey) | ||
.map(OidcMappingKey.class::cast); | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return issuer + "/" + oidcId; | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
src/main/java/com/penguineering/gartenplus/auth/mapping/OidcMappingRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.penguineering.gartenplus.auth.mapping; | ||
|
||
import org.springframework.data.repository.CrudRepository; | ||
|
||
public interface OidcMappingRepository extends CrudRepository<OidcMapping, OidcMappingKey> { | ||
} |
45 changes: 45 additions & 0 deletions
45
src/main/java/com/penguineering/gartenplus/auth/user/UserDTO.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package com.penguineering.gartenplus.auth.user; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnore; | ||
import com.fasterxml.jackson.annotation.JsonInclude; | ||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import org.springframework.web.server.WebSession; | ||
|
||
import java.net.URI; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
import java.util.UUID; | ||
|
||
@JsonInclude(JsonInclude.Include.NON_EMPTY) | ||
public record UserDTO( | ||
@JsonProperty("id") UUID id, | ||
@JsonProperty("display_name") String displayName, | ||
@JsonProperty("email") String email, | ||
@JsonProperty("avatar_url") URI avatarUrl) { | ||
|
||
public UserDTO { | ||
Objects.requireNonNull(displayName, "displayName must not be null"); | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return displayName + "(" + id + ")"; | ||
} | ||
|
||
@JsonIgnore | ||
public boolean isNew() { | ||
return Objects.isNull(id); | ||
} | ||
|
||
public static Optional<UserDTO> fromWebSession(WebSession session) { | ||
return Optional.ofNullable(session.getAttribute("user")) | ||
.filter(user -> user instanceof UserDTO) | ||
.map(UserDTO.class::cast); | ||
} | ||
|
||
public Optional<UserDTO> saveToWebSession(WebSession session) { | ||
return Optional.ofNullable(session.getAttributes().put("user", this)) | ||
.filter(user -> user instanceof UserDTO) | ||
.map(UserDTO.class::cast); | ||
} | ||
} |
Oops, something went wrong.