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
36 changes: 36 additions & 0 deletions deploy/mcp-oauth-proxy/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ data:
google:
paths: "/google/*"
policy: authenticated
slack:
paths: "/slack/*"
policy: authenticated
embrace:
paths: "/embrace/*"
policy: authenticated
Expand Down Expand Up @@ -139,6 +142,36 @@ data:
restore-path-after-redirect: true
extra-params:
access_type: "offline"
slack:
auth-server-url: "https://slack.com"
discovery-enabled: false
authorization-path: "/oauth/v2/authorize"
token-path: "/api/oauth.v2.user.access"
user-info-path: "https://slack.com/api/auth.test"
client-id: {{ .Values.oidc.slack.clientId | quote }}
credentials:
client-secret:
provider:
key: {{ .Values.oidc.slack.clientSecretKey }}
method: post
authentication:
scopes: []
redirect-path: /slack/authorize/callback
restore-path-after-redirect: true
add-openid-scope: false
id-token-required: false
user-info-required: true
verify-access-token: false
extra-params:
user_scope: {{ .Values.oidc.slack.userScopes | quote }}
team: {{ .Values.oidc.slack.team | quote }}
application-type: web-app
allow-user-info-cache: false
cache-user-info-in-idtoken: true
token-state-manager:
strategy: keep-all-tokens
token:
issuer: any
embrace:
{{- if (.Values.oidc.embrace.tlsInsecureClient | default false) }}
tls:
Expand Down Expand Up @@ -339,6 +372,9 @@ data:
- name: splunk
endpoint: {{ $splunk.mgmtEndpoint | default "" | quote }}
username-claim: {{ $splunk.usernameClaim | default "short_id" | quote }}
- name: slack
endpoint: {{ .Values.oidc.slack.endpoint | quote }}
username-claim: {{ .Values.oidc.slack.claim | quote }}
resources:
resource-mapping:
{{- with .Values.application.resourceMapping -}}
Expand Down
7 changes: 7 additions & 0 deletions deploy/mcp-oauth-proxy/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ oidc:
clientSecretKey:
scopes:
- openid
slack:
endpoint: "https://slack.com/oauth/v2/authorize"
clientId:
claim: user_id
clientSecretKey: slack-client-secret
team:
userScopes: "users:read.email,users:read,search:read.users,search:read.public,search:read.private,groups:history,channels:history,canvases:read"
embrace:
authServerUrl: "https://dash-api.embrace.io"
authorizationUrl: "https://dash.embrace.io/oauth/authorize"
Expand Down
131 changes: 131 additions & 0 deletions src/main/java/io/athenz/mop/resource/SlackResource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright The Athenz Authors
*
* 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 io.athenz.mop.resource;

import io.athenz.mop.model.AuthorizationCode;
import io.athenz.mop.service.AuthCodeRegionResolver;
import io.athenz.mop.service.AuthorizerService;
import io.athenz.mop.service.ConfigService;
import io.athenz.mop.service.RefreshTokenService;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.OidcSession;
import io.quarkus.oidc.UserInfo;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.lang.invoke.MethodHandles;
import java.util.Map;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Secondary OAuth callback handler for Slack.
* After Quarkus OIDC completes the Slack OAuth v2 user-scope flow,
* this resource stores the resulting tokens and redirects back to
* the original OAuth client.
*/
@Path("/slack/authorize")
public class SlackResource extends BaseResource {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
static final String PROVIDER = "slack";

@Inject
AuthorizerService authorizerService;

@Inject
AccessTokenCredential accessTokenCredential;

@Inject
AuthCodeRegionResolver authCodeRegionResolver;

@Inject
UserInfo userInfo;

@ConfigProperty(name = "server.token-exchange.idp")
String providerDefault;

@Inject
OidcSession oidcSession;

@Inject
ConfigService configService;

@Inject
RefreshTokenService refreshTokenService;

@GET
@Authenticated
@Produces(MediaType.TEXT_HTML)
public Response authorize(@QueryParam("state") String state) {
if (state == null || state.isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of(
"error", "invalid_request",
"error_description", "Missing state parameter"))
.type(MediaType.APPLICATION_JSON)
.build();
}
log.info("Slack request to store tokens for user: {}", userInfo.get("user_id"));
AuthorizationCode authorizationCode = authCodeRegionResolver.resolve(state, providerDefault).authorizationCode();
if (authorizationCode == null) {
log.warn("Slack callback: authorization code not found for state (local or cross-region)");
return Response.status(Response.Status.BAD_REQUEST)
.entity(Map.of(
"error", "invalid_grant",
"error_description", "Authorization code not found or expired"))
.type(MediaType.APPLICATION_JSON)
.build();
}

String lookupKey = getUsername(userInfo, configService.getRemoteServerUsernameClaim(PROVIDER), null);
String newAccessToken = accessTokenCredential.getToken();
String newRefreshToken = (accessTokenCredential.getRefreshToken() != null)
? accessTokenCredential.getRefreshToken().getToken()
: null;

String existingUpstreamRefresh = refreshTokenService.getUpstreamRefreshToken(authorizationCode.getSubject(), PROVIDER);

String refreshTokenToStore;
if (newRefreshToken != null) {
refreshTokenToStore = newRefreshToken;
log.info("Slack: Using new refresh token from authorization response");
} else if (existingUpstreamRefresh != null) {
refreshTokenToStore = existingUpstreamRefresh;
log.info("Slack: Refresh token not in response, using existing refresh token from upstream table");
} else {
log.warn("Slack: No refresh token received and none found in storage. This may indicate first consent failed.");
logoutFromProvider(PROVIDER, oidcSession);
return Response.serverError().build();
}

authorizerService.storeTokens(
lookupKey,
authorizationCode.getSubject(),
newAccessToken,
newAccessToken,
refreshTokenToStore,
PROVIDER);

logoutFromProvider(PROVIDER, oidcSession);
return buildSuccessRedirect(authorizationCode.getRedirectUri(), state, authorizationCode.getState());
}
}
3 changes: 3 additions & 0 deletions src/main/java/io/athenz/mop/secret/K8SSecretsProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class K8SSecretsProvider implements CredentialsProvider {
public static final String SECRET_DATA_KEY_GITHUB_CLIENT_SECRET = "github-client-secret";
public static final String SECRET_DATA_KEY_GOOGLE_CLIENT_SECRET = "google-client-secret";
public static final String SECRET_DATA_KEY_EMBRACE_CLIENT_SECRET = "embrace-client-secret";
public static final String SECRET_DATA_KEY_SLACK_CLIENT_SECRET = "slack-client-secret";
public static final String SECRET_DATA_KEY_OKTA_TOKEN_EXCHANGE_CLIENT_SECRET = "okta-token-exchange-client-secret";

/** Kubernetes secret data keys for Splunk management API tokens (same names in {@link #getCredentials} map). */
Expand Down Expand Up @@ -92,6 +93,7 @@ static Map<String, String> credentialsMapFromData(Map<String, byte[]> data) {
String githubClientSecret = decodeTrimmed(data, SECRET_DATA_KEY_GITHUB_CLIENT_SECRET);
String googleClientSecret = decodeTrimmed(data, SECRET_DATA_KEY_GOOGLE_CLIENT_SECRET);
String embraceClientSecret = decodeTrimmed(data, SECRET_DATA_KEY_EMBRACE_CLIENT_SECRET);
String slackClientSecret = decodeTrimmed(data, SECRET_DATA_KEY_SLACK_CLIENT_SECRET);
String oktaTokenExchangeClientSecret = decodeTrimmed(data, SECRET_DATA_KEY_OKTA_TOKEN_EXCHANGE_CLIENT_SECRET);
String splunkApiStage = decodeTrimmed(data, SECRET_DATA_KEY_SPLUNK_API_STAGE);
String splunkApiProd = decodeTrimmed(data, SECRET_DATA_KEY_SPLUNK_API_PROD);
Expand All @@ -102,6 +104,7 @@ static Map<String, String> credentialsMapFromData(Map<String, byte[]> data) {
map.put(SECRET_DATA_KEY_GITHUB_CLIENT_SECRET, githubClientSecret);
map.put(SECRET_DATA_KEY_GOOGLE_CLIENT_SECRET, googleClientSecret);
map.put(SECRET_DATA_KEY_EMBRACE_CLIENT_SECRET, embraceClientSecret);
map.put(SECRET_DATA_KEY_SLACK_CLIENT_SECRET, slackClientSecret);
map.put(SECRET_DATA_KEY_OKTA_TOKEN_EXCHANGE_CLIENT_SECRET, oktaTokenExchangeClientSecret);
map.put(SECRET_DATA_KEY_SPLUNK_API_STAGE, splunkApiStage);
map.put(SECRET_DATA_KEY_SPLUNK_API_PROD, splunkApiProd);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ public class TokenExchangeServiceProducer {
@Inject
TokenExchangeServiceDatabricksSqlImpl tokenExchangeServiceDatabricksSqlImpl;

@Inject
TokenExchangeServiceSlackImpl tokenExchangeServiceSlackImpl;

public TokenExchangeService getTokenExchangeServiceImplementation(String idpType) {

return switch (idpType) {
Expand All @@ -61,6 +64,7 @@ public TokenExchangeService getTokenExchangeServiceImplementation(String idpType
case "google-monitoring", "google-logging" -> tokenExchangeServiceGcpWorkforceImpl;
case "splunk" -> tokenExchangeServiceSplunkImpl;
case "databricks-sql" -> tokenExchangeServiceDatabricksSqlImpl;
case "slack" -> tokenExchangeServiceSlackImpl;
default -> throw new IllegalArgumentException("Unsupported IDP type: " + idpType);
};
}
Expand Down
Loading