Skip to content

Commit

Permalink
feat(auth): add support for github SSO / OAuth2 support (#1319)
Browse files Browse the repository at this point in the history
  • Loading branch information
moremagic committed Jan 2, 2023
1 parent 3829e4c commit 3525064
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 3 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ dependencies {

//AWS MSK IAM Auth
implementation group: 'software.amazon.msk', name: 'aws-msk-iam-auth', version: '1.1.5'

// https://mvnrepository.com/artifact/io.projectreactor/reactor-core
implementation group: 'io.projectreactor', name: 'reactor-core', version: '3.5.1'
}

/**********************************************************************************************************************\
Expand Down
6 changes: 4 additions & 2 deletions client/src/containers/Login/Login.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ class Login extends Form {
errors: {},
config: {
formEnabled: true,
oidcAuths: []
oidcAuths: [],
oauthAuths: []
}
};

Expand Down Expand Up @@ -157,7 +158,7 @@ class Login extends Form {
}

render() {
const { formEnabled, oidcAuths } = this.state.config;
const { formEnabled, oidcAuths, oauthAuths } = this.state.config;

return (
<div>
Expand All @@ -177,6 +178,7 @@ class Login extends Form {
{formEnabled && this._renderForm()}
{formEnabled && oidcAuths && this._renderSeparator()}
{oidcAuths && this._renderOidc(oidcAuths)}
{oauthAuths && this._renderOidc(oauthAuths)}
</form>
</main>
</div>
Expand Down
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ module.exports = {
'/docs/configuration/authentifications/basic-auth.md',
'/docs/configuration/authentifications/aws-iam-auth.md',
'/docs/configuration/authentifications/oidc.md',
'/docs/configuration/authentifications/github.md',
'/docs/configuration/authentifications/ldap.md',
'/docs/configuration/authentifications/header.md',
'/docs/configuration/authentifications/external.md',
Expand Down
48 changes: 48 additions & 0 deletions docs/docs/configuration/authentifications/github.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

# GitHub SSO / OAuth2
To enable GitHub SSO in the application, you'll first have to enable OAuth2 in micronaut:

```yaml
micronaut:
security:
enabled: true
oauth2:
enabled: true
clients:
github:
client-id: "<client-id>"
client-secret: "<client-secret>"
scopes:
- user:email
- read:user
authorization:
url: https://github.com/login/oauth/authorize
token:
url: https://github.com/login/oauth/access_token
auth-method: client-secret-post
```

To further tell AKHQ to display GitHub SSO options on the login page and customize claim mapping, configure Oauth in the AKHQ config:

```yaml
akhq:
security:
default-group: no-roles
oauth2:
enabled: true
providers:
github:
label: "Login with GitHub"
username-field: login
users:
- username: franz
groups:
# the corresponding akhq groups (eg. topic-reader/writer or akhq default groups like admin/reader/no-role)
- topic-reader
- topic-writer
```

The username field can be any string field, the roles field has to be a JSON array.

## References
https://micronaut-projects.github.io/micronaut-security/latest/guide/#oauth2-configuration
30 changes: 30 additions & 0 deletions src/main/java/org/akhq/configs/Oauth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.akhq.configs;

import io.micronaut.context.annotation.ConfigurationProperties;
import lombok.Data;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@ConfigurationProperties("akhq.security.oauth2")
@Data
public class Oauth {
private boolean enabled;
private Map<String, Provider> providers;

@Data
public static class Provider {
private String label = "Login with OAuth";
private String usernameField = "login";
private String groupsField = "organizations_url";
private String defaultGroup;
private List<GroupMapping> groups = new ArrayList<>();
private List<UserMapping> users = new ArrayList<>();
}

public Provider getProvider(String key) {
providers.putIfAbsent(key, new Provider());
return providers.get(key);
}
}
19 changes: 19 additions & 0 deletions src/main/java/org/akhq/controllers/AkhqController.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ public class AkhqController extends AbstractController {
@Inject
private Oidc oidc;

@Inject
private Oauth oauth;

@Inject
private UIOptions uIOptions;

Expand Down Expand Up @@ -80,6 +83,13 @@ public AuthDefinition auths() {
.collect(Collectors.toList());
}

if (oauth.isEnabled()) {
authDefinition.oauthAuths = oauth.getProviders().entrySet()
.stream()
.map(e -> new OauthAuth(e.getKey(), e.getValue().getLabel()))
.collect(Collectors.toList());
}

if (applicationContext.containsBean(SecurityService.class)) {
authDefinition.loginEnabled = true;
// Display login form if there are LocalUsers OR Ldap is enabled
Expand Down Expand Up @@ -180,6 +190,7 @@ public static class AuthDefinition {
private boolean loginEnabled;
private boolean formEnabled;
private List<OidcAuth> oidcAuths;
private List<OauthAuth> oauthAuths;
private String version;
}

Expand All @@ -191,6 +202,14 @@ public static class OidcAuth {
private String label;
}

@AllArgsConstructor
@NoArgsConstructor
@Getter
public static class OauthAuth {
private String key;
private String label;
}

@AllArgsConstructor
@NoArgsConstructor
@Getter
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/org/akhq/models/GithubClaims.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.akhq.models;

import io.micronaut.security.token.Claims;

import java.util.HashMap;
import java.util.Set;

public class GithubClaims extends HashMap<String, Object> implements Claims {
@Override
public Object get(String name) {
return super.get(name);
}

@Override
public Set<String> names() {
return super.keySet();
}

@Override
public boolean contains(String name) {
return super.containsKey(name);
}
}
85 changes: 85 additions & 0 deletions src/main/java/org/akhq/modules/GithubAuthenticationMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package org.akhq.modules;

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.util.StringUtils;
import io.micronaut.security.authentication.AuthenticationResponse;
import io.micronaut.security.oauth2.endpoint.authorization.state.State;
import io.micronaut.security.oauth2.endpoint.token.response.OauthAuthenticationMapper;
import io.micronaut.security.oauth2.endpoint.token.response.TokenResponse;
import jakarta.inject.Named;
import org.akhq.configs.Oauth;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.akhq.models.GithubClaims;
import org.akhq.utils.*;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;

import java.util.*;
import java.util.stream.Collectors;

@Singleton
@Named("github")
@Requires(property = "akhq.security.oauth2.enabled", value = StringUtils.TRUE)
public class GithubAuthenticationMapper implements OauthAuthenticationMapper {
@Inject
private Oauth oauth;
@Inject
private GithubApiClient apiClient;
@Inject
private ClaimProvider claimProvider;

@Override
public Publisher<AuthenticationResponse> createAuthenticationResponse(TokenResponse tokenResponse, @Nullable State state) {
return Flux.from(apiClient.getUser("token " + tokenResponse.getAccessToken()))
.map(user -> {
ClaimRequest request = ClaimRequest.builder()
.providerType(ClaimProviderType.OAUTH)
.providerName("github")
.username(getUsername(oauth.getProvider("github"), user))
.groups(getOauthGroups(oauth.getProvider("github"), user))
.build();

ClaimResponse claim = claimProvider.generateClaim(request);

return AuthenticationResponse.success(getUsername(oauth.getProvider("github"), user), claim.getRoles(), claim.getAttributes());
});
}

/**
* Tries to read the username from the configured username field.
*
* @param provider The OAuth provider
* @param user The OAuth claims
* @return The username to set in the {@link io.micronaut.security.authentication.Authentication}
*/
protected String getUsername(Oauth.Provider provider, GithubClaims user) {
String userNameField = provider.getUsernameField();
return Objects.toString(user.get(userNameField));
}

/**
* Tries to read groups from the configured groups field.
* If the configured field cannot be found or isn't some kind of collection, it will return an empty set.
*
* @param provider The OAuth provider configuration
* @param user The OAuth claims
* @return The groups from oauth
*/
protected List<String> getOauthGroups(Oauth.Provider provider, GithubClaims user) {
List<String> groups = new ArrayList<>();
if (user.contains(provider.getGroupsField())) {
Object groupsField = user.get(provider.getGroupsField());
if (groupsField instanceof Collection) {
groups = ((Collection<Object>) groupsField)
.stream()
.map(Objects::toString)
.collect(Collectors.toList());
} else if (groupsField instanceof String) {
groups.add((String) groupsField);
}
}
return groups;
}
}
3 changes: 2 additions & 1 deletion src/main/java/org/akhq/utils/ClaimProviderType.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ public enum ClaimProviderType {
HEADER,
BASIC_AUTH,
LDAP,
OIDC
OIDC,
OAUTH
}
16 changes: 16 additions & 0 deletions src/main/java/org/akhq/utils/GithubApiClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.akhq.utils;

import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Header;
import io.micronaut.http.client.annotation.Client;
import org.akhq.models.GithubClaims;
import org.reactivestreams.Publisher;

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

@Get("/user")
Publisher<GithubClaims> getUser(@Header("Authorization") String authorization);
}
12 changes: 12 additions & 0 deletions src/main/java/org/akhq/utils/LocalSecurityClaimProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class LocalSecurityClaimProvider implements ClaimProvider {
Ldap ldapProperties;
@Inject
Oidc oidcProperties;
@Inject
Oauth oauthProperties;

@Override
public ClaimResponse generateClaim(ClaimRequest request) {
Expand Down Expand Up @@ -63,6 +65,16 @@ public ClaimResponse generateClaim(ClaimRequest request) {
defaultGroup = provider.getDefaultGroup();
akhqGroups.addAll(mapToAkhqGroups(request.getUsername(), request.getGroups(), groupMappings, userMappings, defaultGroup));
break;
case OAUTH:
// we need to convert from OAUTH login name to AKHQ groups to find the roles and attributes
// using akhq.security.oauth2.groups and akhq.security.oauth2.users
// as well as akhq.security.oauth2.default-group
Oauth.Provider oauthPropertiesProvider = oauthProperties.getProvider(request.getProviderName());
userMappings = oauthPropertiesProvider.getUsers();
groupMappings = oauthPropertiesProvider.getGroups();
defaultGroup = oauthPropertiesProvider.getDefaultGroup();
akhqGroups.addAll(mapToAkhqGroups(request.getUsername(), request.getGroups(), groupMappings, userMappings, defaultGroup));
break;
default:
break;
}
Expand Down

0 comments on commit 3525064

Please sign in to comment.