Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KNOX-3016 - add support for client credentials flow #876

Merged
merged 4 commits into from
Mar 12, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ public abstract class AbstractJWTFilter implements Filter {
*/
public static final String JWT_EXPECTED_SIGALG = "jwt.expected.sigalg";
public static final String JWT_DEFAULT_SIGALG = "RS256";
public static final String TYPE = "type";
public static final String CLIENT_ID = "CLIENT_ID";

static JWTMessages log = MessagesFactory.get( JWTMessages.class );

Expand Down Expand Up @@ -300,8 +302,23 @@ protected Subject createSubjectFromToken(final JWT token) throws UnknownTokenExc

public Subject createSubjectFromTokenIdentifier(final String tokenId) throws UnknownTokenException {
TokenMetadata metadata = tokenStateService.getTokenMetadata(tokenId);
String username = null;
if (metadata != null) {
return createSubjectFromTokenData(metadata.getUserName(), null);
String type = metadata.getMetadata(TYPE);
lmccay marked this conversation as resolved.
Show resolved Hide resolved
// using tokenID and passcode as CLIENT_ID and CLIENT_SECRET will
// result in a metadata item called "type". If the value is set
// to CLIENT_ID then it will be assumed to be a CLIENT_ID and we
// will use the token id as the username. Since we don't know the
// token id until it is created, the username is always the same
// in the record. Using the token id makes it a unique username for
// audit and the like.
if (CLIENT_ID.equalsIgnoreCase(type)) {
username = tokenId;
}
else {
username = metadata.getUserName();
}
return createSubjectFromTokenData(username, null);
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public class JWTFederationFilter extends AbstractJWTFilter {
private static final JWTMessages LOGGER = MessagesFactory.get( JWTMessages.class );
/* A semicolon separated list of paths that need to bypass authentication */
public static final String JWT_UNAUTHENTICATED_PATHS_PARAM = "jwt.unauthenticated.path.list";
public static final String GRANT_TYPE = "grant_type";
public static final String CLIENT_CREDENTIALS = "client_credentials";
public static final String CLIENT_SECRET = "client_secret";

public enum TokenType {
JWT, Passcode;
Expand Down Expand Up @@ -239,15 +242,45 @@ public Pair<TokenType, String> getWireToken(final ServletRequest request) {
}

if (parsed == null) {
token = request.getParameter(this.paramName);
if (token != null) {
parsed = Pair.of(TokenType.JWT, token);
}
parsed = parseFromClientCredentialsFlow(request);
}

if (parsed == null) {
token = request.getParameter(this.paramName);
if (token != null) {
parsed = Pair.of(TokenType.JWT, token);
}
}

return parsed;
}

private Pair<TokenType, String> parseFromClientCredentialsFlow(ServletRequest request) {
Pair<TokenType, String> parsed = null;
String token = null;

/*
POST /{tenant}/oauth2/v2.0/token HTTP/1.1
Host: login.microsoftonline.com:443
Content-Type: application/x-www-form-urlencoded

client_id=535fb089-9ff3-47b6-9bfb-4f1264799865
&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default
&client_secret=sampleCredentials
&grant_type=client_credentials
*/

String grantType = request.getParameter(GRANT_TYPE);
if (CLIENT_CREDENTIALS.equals(grantType)) {
// this is indeed a client credentials flow client_id and
// client_secret are expected now the client_id will be in
// the token as the token_id so we will get that later
token = request.getParameter(CLIENT_SECRET);
parsed = Pair.of(TokenType.Passcode, token);
}
return parsed;
}

private Pair<TokenType, String> parseFromHTTPBasicCredentials(final String header) {
Pair<TokenType, String> parsed = null;
final String base64Credentials = header.substring(BASIC.length()).trim();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with this
* work for additional information regarding copyright ownership. The ASF
* licenses this file to you 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.apache.knox.gateway.provider.federation;


import org.easymock.EasyMock;
import org.junit.Test;

import javax.servlet.http.HttpServletRequest;


public class ClientIdAndClientSecretFederationFilterTest extends TokenIDAsHTTPBasicCredsFederationFilterTest {
@Override
protected void setTokenOnRequest(HttpServletRequest request, String authUsername, String authPassword) {
EasyMock.expect((Object)request.getHeader("Authorization")).andReturn("");
EasyMock.expect((Object)request.getParameter("grant_type")).andReturn("client_credentials");
EasyMock.expect((Object)request.getParameter("client_id")).andReturn(authUsername);
EasyMock.expect((Object)request.getParameter("client_secret")).andReturn(authPassword);
}

@Override
@Test
public void testInvalidUsername() throws Exception {
// there is no way to specify an invalid username for
// client credentials flow or at least no meaningful way
// to do so for our implementation. The client id is
// actually encoded in the client secret and that is used
// for the actual authentication with passcodes.
}

@Override
@Test
public void testInvalidJWTForPasscode() throws Exception {
// there is no way to specify an invalid username for
// client credentials flow or at least no meaningful way
// to do so for our implementation. The username is actually
// set by the JWTProvider when determining that the request
// is a client credentials flow.
}
}
Loading