Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 5 additions & 1 deletion .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 OAuth provider.
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 Expand Up @@ -74,4 +78,4 @@ STUDY_START_DELAY=10s
TRIAL_START_DELAY=15s
TRAIT_START_DELAY=20s
OBSERVATION_START_DELAY=25s
OBSERVATION_UNIT_START_DELAY=30s
OBSERVATION_UNIT_START_DELAY=30s
4 changes: 3 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,7 @@ jobs:
JWT_SECRET: ${{ secrets.JWT_SECRET }}
OAUTH_CLIENT_ID: 123abc
OAUTH_CLIENT_SECRET: asdfljkhalkbaldsfjasdfi238497098asdf
GITHUB_OAUTH_CLIENT_ID: 12345678901234567890
GITHUB_OAUTH_CLIENT_SECRET: 1234567890123456789012345678901234567890
BRAPI_REFERENCE_SOURCE: breedinginsight.org
BRAPI_DOCKER_IMAGE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.brapi_server_image || 'breedinginsight/brapi-java-server:develop' }}
BRAPI_DOCKER_IMAGE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.brapi_server_image || 'breedinginsight/brapi-java-server:develop' }}
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ services:
- API_INTERNAL_PORT=${API_INTERNAL_PORT}
- API_INTERNAL_TEST_PORT=${API_INTERNAL_TEST_PORT}
- OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
- GITHUB_OAUTH_CLIENT_ID=${GITHUB_OAUTH_CLIENT_ID}
- GITHUB_OAUTH_CLIENT_SECRET=${GITHUB_OAUTH_CLIENT_SECRET}
- JWT_DOMAIN=${JWT_DOMAIN}
- DB_SERVER=${DB_SERVER}
- DB_NAME=${DB_NAME}
Expand Down
9 changes: 9 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 Expand Up @@ -619,6 +624,10 @@
<url>jdbc:postgresql://${DB_SERVER}/${DB_NAME}</url>
<user>${DB_USER}</user>
<password>${DB_PASSWORD}</password>
<locations>
<location>filesystem:src/main/java/org/breedinginsight/db/migration</location>
<location>filesystem:src/main/resources/db/migration</location>
</locations>
</configuration>
<dependencies>
<dependency>
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());
}
}
Loading