Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
USER_ID=<user id of system user>
GROUP_ID=<group id of system user>

# GitHub OAuth variables. Only required if using GitHub as an alternative to ORCID.
GITHUB_OAUTH_CLIENT_ID=<Client ID of GitHub OAuth app.>
GITHUB_OAUTH_CLIENT_SECRET=<Client Secret of GitHub Oauth app.>

ORCID_SANDBOX_AUTHENTICATION=<true or false; true=>use the Sandbox Orcid, false=>use the Production Orcid. Defaults to false.>

# Authentication variables
Expand Down
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@
<artifactId>micronaut-inject</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-client</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-validation</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import io.micronaut.security.token.jwt.cookie.JwtCookieLoginHandler;
import io.micronaut.security.token.jwt.generator.AccessRefreshTokenGenerator;
import io.micronaut.security.token.jwt.generator.AccessTokenConfiguration;
import io.micronaut.security.token.jwt.generator.JwtGeneratorConfiguration;
import lombok.extern.slf4j.Slf4j;
import org.breedinginsight.api.model.v1.auth.SignUpJWT;
import org.breedinginsight.model.ProgramUser;
Expand Down Expand Up @@ -83,7 +82,7 @@ public AuthServiceLoginHandler(JwtCookieConfiguration jwtCookieConfiguration,

@Override
public MutableHttpResponse<?> loginSuccess(UserDetails userDetails, HttpRequest<?> request) {
// Called when login to orcid is successful.
// Called when login to OAuth provider is successful.
// Check if our login to our system is successful.
if (request.getCookies().contains(accountTokenCookieName)) {
Cookie accountTokenCookie = request.getCookies().get(accountTokenCookieName);
Expand Down Expand Up @@ -124,7 +123,7 @@ public MutableHttpResponse<?> loginSuccess(UserDetails userDetails, HttpRequest<

private AuthenticatedUser getUserCredentials(UserDetails userDetails) throws AuthenticationException {

Optional<User> user = userService.getByOrcid(userDetails.getUsername());
Optional<User> user = userService.getByOAuthId(userDetails.getUsername());

if (user.isPresent()) {
if (user.get().getActive()) {
Expand Down Expand Up @@ -159,9 +158,20 @@ public MutableHttpResponse<?> loginFailed(AuthenticationResponse authenticationF
}
}

private String parseOAuthProvider(HttpRequest request) {
// The request path will be something like "/sso/success/github".
if (request.getPath().toLowerCase().contains("github")) {
return "github";
} else {
// Default to ORCID.
return "orcid";
}
}

private MutableHttpResponse newAccountCreationResponse(UserDetails userDetails, String accountToken, HttpRequest request) {

String orcid = userDetails.getUsername();
String oAuthId = userDetails.getUsername();
String oAuthProvider = parseOAuthProvider(request);
SignUpJWT signUpJWT;
try {
signUpJWT = signUpJwtService.validateAndParseAccountSignUpJwt(accountToken);
Expand All @@ -185,9 +195,9 @@ private MutableHttpResponse newAccountCreationResponse(UserDetails userDetails,
}

if (newUser.getAccountToken().equals(signUpJWT.getJwtId().toString())) {
// Assign orcid to that user
// Assign OAuth Id and provider to that user.
try {
userService.updateOrcid(newUser.getId(), orcid);
userService.updateOAuthInfo(newUser.getId(), oAuthId, oAuthProvider);
} catch (DoesNotExistException e) {
MutableHttpResponse resp = HttpResponse.seeOther(URI.create(newAccountErrorUrl));
return resp;
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/org/breedinginsight/api/auth/GithubApiClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.breedinginsight.api.auth;

import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Header;
import io.micronaut.http.client.annotation.Client;
import io.reactivex.Flowable;

@Header(name = "User-Agent", value = "Micronaut")
@Client("https://api.github.com")
public interface GithubApiClient {

@Get("/user")
Flowable<GithubUser> getUser(@Header("Authorization") String authorization);
}

37 changes: 37 additions & 0 deletions src/main/java/org/breedinginsight/api/auth/GithubUser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.breedinginsight.api.auth;

import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import io.micronaut.core.annotation.Introspected;
import lombok.Getter;

@Introspected
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
@Getter
public class GithubUser {

private String id;
// The login will be the unique GitHub username.
private String login;
private String name;
private String email;

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.breedinginsight.api.auth;

import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.async.publisher.Publishers;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.authentication.UserDetails;
import io.micronaut.security.oauth2.endpoint.authorization.state.State;
import io.micronaut.security.oauth2.endpoint.token.response.OauthUserDetailsMapper;
import io.micronaut.security.oauth2.endpoint.token.response.TokenResponse;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;


import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Collections;
import java.util.List;

@Slf4j
@Named("github")
@Singleton
class GithubUserDetailsMapper implements OauthUserDetailsMapper {

private final GithubApiClient apiClient;

GithubUserDetailsMapper(GithubApiClient apiClient) {
this.apiClient = apiClient;
}

@Override
public Publisher<UserDetails> createUserDetails(TokenResponse tokenResponse) {
return Publishers.just(new UnsupportedOperationException());
}

@Override
public Publisher<AuthenticationResponse> createAuthenticationResponse(TokenResponse tokenResponse, @Nullable State state) {
return apiClient.getUser("token " + tokenResponse.getAccessToken())
.map(user -> {
List<String> roles = Collections.singletonList("ROLE_GITHUB");
return new UserDetails(user.getLogin(), roles);
});
}
}

4 changes: 2 additions & 2 deletions src/main/java/org/breedinginsight/daos/ProgramUserDAO.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,14 +120,14 @@ public List<ProgramUser> getProgramUsersByUserId(UUID userId) {
return parseRecords(records, createdByUser, updatedByUser);
}

public List<ProgramUser> getProgramUsersByOrcid(String orcid) {
public List<ProgramUser> getProgramUsersByOAuthId(String oAuthId) {

BiUserTable createdByUser = BI_USER.as("createdByUser");
BiUserTable updatedByUser = BI_USER.as("updatedByUser");

// TODO: When we allow for pulling archived users, active condition won't be hardcoded.
Result<Record> records = getProgramUsersQuery(createdByUser, updatedByUser)
.where(BI_USER.ORCID.eq(orcid))
.where(BI_USER.OAUTH_ID.eq(oAuthId))
.and(PROGRAM.ACTIVE.eq(true))
.fetch();

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/breedinginsight/daos/UserDAO.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ public interface UserDAO extends DAO<BiUserRecord, BiUserEntity, UUID> {

Optional<User> getUser(UUID id);

Optional<User> getUserByOrcId(String orcid);
Optional<User> getUserByOAuthId(String oAuthId);

BiUserEntity fetchOneById(UUID value);

List<BiUserEntity> fetchByEmail(String... values);

List<BiUserEntity> fetchByOrcid(String... values);
List<BiUserEntity> fetchByOauthId(String... values);
}
6 changes: 3 additions & 3 deletions src/main/java/org/breedinginsight/daos/impl/UserDAOImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ public Optional<User> getUser(UUID id) {
return Utilities.getSingleOptional(users);
}

public Optional<User> getUserByOrcId(String orcid) {
public Optional<User> getUserByOAuthId(String oAuthId) {
List<Record> records = getUsersQuery()
.where(BI_USER.ORCID.eq(orcid))
.where(BI_USER.OAUTH_ID.eq(oAuthId))
.fetch();
List<ProgramUser> programUsers = programUserDAO.getProgramUsersByOrcid(orcid);
List<ProgramUser> programUsers = programUserDAO.getProgramUsersByOAuthId(oAuthId);
List<User> users = parseRecords(records, programUsers);

return Utilities.getSingleOptional(users);
Expand Down
13 changes: 8 additions & 5 deletions src/main/java/org/breedinginsight/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,14 @@ public class User extends BiUserEntity {

public User(BiUserEntity biUser) {
this.setId(biUser.getId());
this.setOrcid(biUser.getOrcid());
this.setOauthId(biUser.getOauthId());
this.setName(biUser.getName());
this.setEmail(biUser.getEmail());
this.setSystemRoles(new ArrayList<>());
this.setProgramRoles(new ArrayList<>());
this.setActive(biUser.getActive());
this.setAccountToken(biUser.getAccountToken());
this.setOauthProvider(biUser.getOauthProvider());
}

public User() {
Expand All @@ -72,13 +73,14 @@ public User() {
public static User parseSQLRecord(Record record, @NotNull BiUserTable tableName){
return User.builder()
.id(record.getValue(tableName.ID))
.orcid(record.getValue(tableName.ORCID))
.oauthId(record.getValue(tableName.OAUTH_ID))
.name(record.getValue(tableName.NAME))
.email(record.getValue(tableName.EMAIL))
.systemRoles(new ArrayList<>())
.programRoles(new ArrayList<>())
.active(record.getValue(tableName.ACTIVE))
.accountToken(record.getValue(tableName.ACCOUNT_TOKEN))
.oauthProvider(record.getValue(tableName.OAUTH_PROVIDER))
.build();
}

Expand All @@ -98,19 +100,20 @@ public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(getId(), user.getId()) &&
Objects.equals(getOrcid(), user.getOrcid()) &&
Objects.equals(getOauthId(), user.getOauthId()) &&
Objects.equals(getName(), user.getName()) &&
Objects.equals(getEmail(), user.getEmail()) &&
Objects.equals(getCreatedAt(), user.getCreatedAt()) &&
Objects.equals(getUpdatedAt(), user.getUpdatedAt()) &&
Objects.equals(getCreatedBy(), user.getCreatedBy()) &&
Objects.equals(getUpdatedBy(), user.getUpdatedBy()) &&
Objects.equals(getActive(), user.getActive()) &&
Objects.equals(getAccountToken(), user.getAccountToken());
Objects.equals(getAccountToken(), user.getAccountToken()) &&
Objects.equals(getOauthProvider(), user.getOauthProvider());
}

@Override
public int hashCode() {
return Objects.hash(getId(), getOrcid(), getName(), getEmail(), getCreatedAt(), getUpdatedAt(), getCreatedBy(), getUpdatedBy(), getActive(), getAccountToken());
return Objects.hash(getId(), getOauthId(), getName(), getEmail(), getCreatedAt(), getUpdatedAt(), getCreatedBy(), getUpdatedBy(), getActive(), getAccountToken(), getOauthProvider());
}
}
23 changes: 12 additions & 11 deletions src/main/java/org/breedinginsight/services/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ public UserService(UserDAO dao, SystemUserRoleDao systemUserRoleDao, SystemRoleD
}


public Optional<User> getByOrcid(String orcid) {
public Optional<User> getByOAuthId(String oAuthId) {

// User has been authenticated against orcid, check they have a bi account.
Optional<User> users = dao.getUserByOrcId(orcid);
// User has been authenticated against OAuth provider, check they have a bi account.
Optional<User> users = dao.getUserByOAuthId(oAuthId);

if (users.isEmpty()) {
return Optional.empty();
Expand All @@ -113,7 +113,7 @@ public List<User> getAll() {

public Optional<User> getById(UUID userId) {

// User has been authenticated against orcid, check they have a bi account.
// User has been authenticated against OAuth provider, check they have a bi account.
Optional<User> user = dao.getUser(userId);

if (!user.isPresent()) {
Expand Down Expand Up @@ -146,7 +146,7 @@ public User create(AuthenticatedUser actingUser, UserRequest userRequest, Config
insertSystemRoles(actingUser, newUser.getId(), systemRoles);
}

// Start OrcID association flow
// Start OAuth account association flow
createAndSendAccountToken(newUser.getId());

return getById(newUser.getId()).get();
Expand Down Expand Up @@ -352,22 +352,23 @@ public void createAndSendAccountToken(UUID userId) throws DoesNotExistException
sendAccountSignUpEmail(biUser, jwt.getSignedJWT());
}

public void updateOrcid(UUID userId, String orcid) throws DoesNotExistException, AlreadyExistsException {
public void updateOAuthInfo(UUID userId, String oAuthId, String oAuthProvider) throws DoesNotExistException, AlreadyExistsException {

BiUserEntity biUser = dao.fetchOneById(userId);

if (biUser == null) {
throw new DoesNotExistException("UUID for user does not exist");
}

List<BiUserEntity> biUserWithOrcidList = dao.fetchByOrcid(orcid);
for (BiUserEntity biUserWithOrcid: biUserWithOrcidList){
if (!biUserWithOrcid.getId().equals(userId)){
throw new AlreadyExistsException("Orcid already in use");
List<BiUserEntity> biUserWithOAuthIdList = dao.fetchByOauthId(oAuthId);
for (BiUserEntity biUserWithOAuthId: biUserWithOAuthIdList){
if (!biUserWithOAuthId.getId().equals(userId)){
throw new AlreadyExistsException("OAuth Id already in use");
}
}

biUser.setOrcid(orcid);
biUser.setOauthId(oAuthId);
biUser.setOauthProvider(oAuthProvider);
biUser.setAccountToken(null);
dao.update(biUser);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public UserQueryMapper() {
fields = Map.ofEntries(
Map.entry("name", User::getName),
Map.entry("email", User::getEmail),
Map.entry("orcid", User::getOrcid),
Map.entry("oauthId", User::getOauthId),
Map.entry("systemRoles",
user -> user.getSystemRoles() != null ? user.getSystemRoles().stream()
.map(role -> role.getDomain()).collect(Collectors.toList()) : null),
Expand Down
Loading
Loading