Skip to content

Fixes gh-4001 : CSRF token BREACH Attack #8082

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

Closed
wants to merge 1 commit into from
Closed
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
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -640,7 +640,7 @@ public void match(MvcResult result) throws Exception {
MockHttpServletRequest request = result.getRequest();
CsrfToken token = WebTestUtils.getCsrfTokenRepository(request).loadToken(request);
assertThat(token).isNotNull();
assertThat(token.getToken()).isEqualTo(this.token.apply(result));
assertThat(token.matches(this.token.apply(result))).isTrue();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,3 +411,10 @@ Overriding the HTTP method occurs in a filter.
That filter must be placed before Spring Security's support.
Note that overriding only happens on a `post`, so this is actually unlikely to cause any real problems.
However, it is still best practice to ensure it is placed before Spring Security's filters.


[[csrf-considerations-breach-mitigation]]
==== BREACH Security Exploit Mitigation
In order to protect our application from BREACH attacks https://en.wikipedia.org/wiki/BREACH consider to use `XorCsrfToken` implementation instead of `DefaultCsrfToken`.
`XorCsrfToken` implementation provides protection from BREACH by always returning random string for the token value.
this will make it harder to guess the actual length of each response payload when using HTTP compression.
59 changes: 59 additions & 0 deletions docs/manual/src/docs/asciidoc/_includes/servlet/exploits/csrf.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -414,3 +414,62 @@ We have <<csrf-considerations-multipart-body,already discussed>> the trade-offs

In Spring's Servlet support, overriding the HTTP method is done using https://docs.spring.io/spring-framework/docs/5.2.x/javadoc-api/org/springframework/web/filter/reactive/HiddenHttpMethodFilter.html[HiddenHttpMethodFilter].
More information can be found in https://docs.spring.io/spring/docs/5.2.x/spring-framework-reference/web.html#mvc-rest-method-conversion[HTTP Method Conversion] section of the reference documentation.

[[servlet-csrf-considerations-breach-mitigation]]
=== BREACH Security Exploit Mitigation
We have <<csrf-considerations-breach-mitigation,already discussed>> usage on `XorCsrfToken` implementation.

In Spring's Servlet support, https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/CookieCsrfTokenRepository.html[CookieCsrfTokenRepository] and https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/HttpSessionCsrfTokenRepository.html[HttpSessionCsrfTokenRepository] use https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/DefaultCsrfToken.html[DefaultCsrfToken] instance type as return type for method `generateToken(HttpServletRequest request)` and `loadToken(HttpServletRequest request)`.

In order to override the default implementation in both classes `setGenerateToken(GenerateTokenProvider<? extends CsrfToken> generateTokenProvider)` is introduced.
We could set the https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/GenerateTokenProvider.html[GenerateTokenProvider] from provided static method https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/XorCsrfToken.html[XorCsrfToken.createGenerateTokenProvider()] as per below:

.setGenerateToken from provided static method
====
[source,java]
----
CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository();
cookieCsrfTokenRepository.setGenerateToken(XorCsrfToken.createGenerateTokenProvider())
----
====

or we could set by defining our own implementation

.setGenerateToken own implementation
====
[source,java]
----
CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository();
cookieCsrfTokenRepository.setGenerateToken((headerName, parameterName, value) -> new XorCsrfToken(headerName, parameterName, value))
----
====

Note that `GenerateTokenProvider` is a `FunctionalInterface` that accepts three parameters `(headerName, parameterName, value)`,
`headerName` and `parameterName` values are derived/assigned from `CookieCsrfTokenRepository` / `HttpSessionCsrfTokenRepository` fields,
`value` is a string token value generated/loaded from `CookieCsrfTokenRepository` / `HttpSessionCsrfTokenRepository`.

We could customize `headerName` and `parameterName` in a couple of ways:

.setGenerateToken hard coded
====
[source,java]
----
CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository();
cookieCsrfTokenRepository.setGenerateToken((headerName, parameterName, value) -> new XorCsrfToken("customHeader", "customParameter", value));
----
====

.setGenerateToken from our own configuration instance
====
[source,java]
----
ExampleConfig exampleConfig = new ExampleConfig();

CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository();
cookieCsrfTokenRepository.setGenerateToken((headerName, parameterName, value) -> new XorCsrfToken(exampleConfig.getHeaderName(), exampleConfig.getParameterName(), value))
----
====

This means in the future `headerName` and `parameterName` will be maintained outside of `CookieCsrfTokenRepository` / `HttpSessionCsrfTokenRepository` classes,
so `setHeaderName(String headerName)` and `setParameterName(String parameterName)` are deprecated and might be removed in the future version.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -58,7 +58,7 @@ public Message<?> preSend(Message<?> message, MessageChannel channel) {
String actualTokenValue = SimpMessageHeaderAccessor.wrap(message)
.getFirstNativeHeader(expectedToken.getHeaderName());

boolean csrfCheckPassed = expectedToken.getToken().equals(actualTokenValue);
boolean csrfCheckPassed = expectedToken.matches(actualTokenValue);
if (csrfCheckPassed) {
return message;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -44,8 +44,7 @@ public void defaults() {
assertThat(request.getParameter("username")).isEqualTo("user");
assertThat(request.getParameter("password")).isEqualTo("password");
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getParameter(token.getParameterName()))
.isEqualTo(token.getToken());
assertThat(token.matches(request.getParameter(token.getParameterName()))).isTrue();
assertThat(request.getRequestURI()).isEqualTo("/login");
assertThat(request.getParameter("_csrf")).isNotNull();
}
Expand All @@ -61,8 +60,7 @@ public void custom() {
assertThat(request.getParameter("username")).isEqualTo("admin");
assertThat(request.getParameter("password")).isEqualTo("secret");
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getParameter(token.getParameterName()))
.isEqualTo(token.getToken());
assertThat(token.matches(request.getParameter(token.getParameterName()))).isTrue();
assertThat(request.getRequestURI()).isEqualTo("/login");
}

Expand All @@ -77,8 +75,7 @@ public void customWithUriVars() {
assertThat(request.getParameter("username")).isEqualTo("admin");
assertThat(request.getParameter("password")).isEqualTo("secret");
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getParameter(token.getParameterName()))
.isEqualTo(token.getToken());
assertThat(token.matches(request.getParameter(token.getParameterName()))).isTrue();
assertThat(request.getRequestURI()).isEqualTo("/uri-login/val1/val2");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -40,8 +40,7 @@ public void defaults() {
CsrfToken token = (CsrfToken) request.getAttribute(CsrfRequestPostProcessor.TestCsrfTokenRepository.TOKEN_ATTR_NAME);

assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getParameter(token.getParameterName())).isEqualTo(
token.getToken());
assertThat(token.matches(request.getParameter(token.getParameterName()))).isTrue();
assertThat(request.getRequestURI()).isEqualTo("/logout");
}

Expand All @@ -53,8 +52,7 @@ public void custom() {
CsrfToken token = (CsrfToken) request.getAttribute(CsrfRequestPostProcessor.TestCsrfTokenRepository.TOKEN_ATTR_NAME);

assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getParameter(token.getParameterName())).isEqualTo(
token.getToken());
assertThat(token.matches(request.getParameter(token.getParameterName()))).isTrue();
assertThat(request.getRequestURI()).isEqualTo("/admin/logout");
}

Expand All @@ -66,8 +64,7 @@ public void customWithUriVars() {
CsrfToken token = (CsrfToken) request.getAttribute(CsrfRequestPostProcessor.TestCsrfTokenRepository.TOKEN_ATTR_NAME);

assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getParameter(token.getParameterName())).isEqualTo(
token.getToken());
assertThat(token.matches(request.getParameter(token.getParameterName()))).isTrue();
assertThat(request.getRequestURI()).isEqualTo("/uri-logout/val1/val2");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,7 +17,7 @@
package org.springframework.security.web.csrf;

import java.util.UUID;

import java.util.function.Function;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
Expand Down Expand Up @@ -53,13 +53,15 @@ public final class CookieCsrfTokenRepository implements CsrfTokenRepository {

private String cookieDomain;

private Function<String, CsrfToken> generateTokenProvider = (value) -> new DefaultCsrfToken(this.headerName,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than passing in createNewToken() as an argument to the Function this should be a Producer and it can create the token itself.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same generateTokenProvider found in CookieCsrfTokenRepository. We could move this logic as a static method on DefaultCsrfToken just like the static methods on XorCsrfToken.

Copy link
Contributor Author

@rh-id rh-id Apr 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the loadToken method for
CookieCsrfTokenRepository required to passed in string token as value and construct the CsrfToken instance which is why it shouldn't be a Producer that can create token itself.

@Override
public CsrfToken loadToken(HttpServletRequest request) {
	Cookie cookie = WebUtils.getCookie(request, this.cookieName);
	if (cookie == null) {
		return null;
	}
	String token = cookie.getValue();
	if (!StringUtils.hasLength(token)) {
		return null;
	}
	return this.generateTokenProvider.apply(token);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even if it was replaced with something like Producer<CsrfToken> generator = new DefaultCsrfTokenGenerator(parameterName, headerName);

it will fail on this test

@Test
public void generateTokenCustom() {
	String headerName = "headerName";
	String parameterName = "paramName";
	this.repository.setHeaderName(headerName);
	this.repository.setParameterName(parameterName);

	CsrfToken generateToken = this.repository.generateToken(this.request);

	assertThat(generateToken).isNotNull();
	assertThat(generateToken.getHeaderName()).isEqualTo(headerName);
	assertThat(generateToken.getParameterName()).isEqualTo(parameterName);
	assertThat(generateToken.getToken()).isNotEmpty();
}

because the headerName and parameterName was not in sync

it can be solved by setting it together something like :

public void setParameterName(String parameterName) {
	Assert.notNull(parameterName, "parameterName is not null");
	this.parameterName = parameterName;
	this.generateTokenProvider.setParameterName(parameterName);
}

however it will caused another issue for another kind of test:

	String headerName = "headerName";
	String parameterName = "paramName";
	this.repository.setHeaderName(headerName);
	this.repository.setParameterName(parameterName);
	Producer prod = new DefaultCsrfTokenGenerator(headerName, parameterName);

	this.repository.setGenerateToken(prod);
	prod.setHeaderName("otherHeader");

	CsrfToken generateToken = this.repository.generateToken(this.request);

	assertThat(generateToken).isNotNull();
	assertThat(generateToken.getHeaderName()).isEqualTo(headerName); // this will fail
	assertThat(generateToken.getParameterName()).isEqualTo(parameterName);
	assertThat(generateToken.getToken()).isNotEmpty();

the above test will fail because the value was not in sync

Copy link
Contributor Author

@rh-id rh-id Apr 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Function & FunctionalInterface approach ensures that we are passing this.headerName and this.parameterName as reference so that it will always be in sync

this is to ensure backwards compatibility for setParameterName and setHeaderName while depreciating it before removed in the future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. I think you are right that the default generator needs to be able to access the headerName and parameterName values in the event they are updated. Alternatively, setting the headerName and parameterName need to also update the generator.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is this approach fine ? should we deprecate the setHeaderName and setParameterName in both CookieCsrfTokenRepository and HttpSessionCsrfTokenRepository as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

this.parameterName, value);

public CookieCsrfTokenRepository() {
}

@Override
public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.headerName, this.parameterName,
createNewToken());
return this.generateTokenProvider.apply(createNewToken());
}

@Override
Expand Down Expand Up @@ -97,15 +99,17 @@ public CsrfToken loadToken(HttpServletRequest request) {
if (!StringUtils.hasLength(token)) {
return null;
}
return new DefaultCsrfToken(this.headerName, this.parameterName, token);
return this.generateTokenProvider.apply(token);
}

/**
* Sets the name of the HTTP request parameter that should be used to provide a token.
*
* @param parameterName the name of the HTTP request parameter that should be used to
* provide a token
* @deprecated use {@link #setGenerateToken(generateTokenProvider)} and pass the parameterName instead.
*/
@Deprecated
public void setParameterName(String parameterName) {
Assert.notNull(parameterName, "parameterName is not null");
this.parameterName = parameterName;
Expand All @@ -116,7 +120,9 @@ public void setParameterName(String parameterName) {
*
* @param headerName the name of the HTTP header that should be used to provide the
* token
* @deprecated use {@link #setGenerateToken(generateTokenProvider)} and pass the headerName instead.
*/
@Deprecated
public void setHeaderName(String headerName) {
Assert.notNull(headerName, "headerName is not null");
this.headerName = headerName;
Expand Down Expand Up @@ -195,4 +201,21 @@ public void setCookieDomain(String cookieDomain) {
this.cookieDomain = cookieDomain;
}

/**
* Sets generate token provider<br/>
* <br/>
* Example : <br/>
* <br/>
* {@code (headerName, parameterName, value) -> new DefaultCsrfToken(headerName, parameterName, value)}<br/>
*
* @param generateTokenProvider provider to be used for generateToken and
* loadToken
* @since 5.4
* @see GenerateTokenProvider
* @see XorCsrfToken
*/
public void setGenerateToken(GenerateTokenProvider<? extends CsrfToken> generateTokenProvider) {
this.generateTokenProvider = (value) -> generateTokenProvider.generateToken(this.headerName, this.parameterName,
value);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -122,7 +122,7 @@ protected void doFilterInternal(HttpServletRequest request,
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (!csrfToken.matches(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -21,6 +21,7 @@
* Provides the information about an expected CSRF token.
*
* @see DefaultCsrfToken
* @see XorCsrfToken
*
* @author Rob Winch
* @since 3.2
Expand Down Expand Up @@ -49,4 +50,14 @@ public interface CsrfToken extends Serializable {
*/
String getToken();

/**
* Compare if this token matches with another token.
*
* @param token to be matched
* @return true if this instance token matches the token, otherwise false.
* @since 5.4
*/
default boolean matches(String token) {
return getToken().equals(token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2002-2020 the original author or 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
*
* https://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.springframework.security.web.csrf;

/**
* Functional interface to provide CSRF token generation logic
*
* @author Ruby Hartono
*
* @param <T> the type of the returned CsrfToken
* @since 5.4
*/
@FunctionalInterface
public interface GenerateTokenProvider<T extends CsrfToken> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't need this interface.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you missed this comment. We shouldn't need this interface. We should use a Producer<CsrfToken> instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need this interface. As mentioned from the other comments.

Do you mean to rename this from GenerateTokenProvider<T extends CsrfToken> to Producer<CsrfToken> ?

Copy link
Member

@rwinch rwinch May 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh sorry. I meant to say Supplier<CsrfToken.

All occurrences of GenerateTokenProvider can be replaced with a Supplier<CsrfToken>.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mean java.util.function.Supplier ?

nope, it cannot be replaced. GenerateTokenProvider accepts 3 arguments (headerName, parameterName, and value) while Supplier accepts no arguments.

Copy link
Member

@rwinch rwinch May 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be done as constructor arguments to the Supplier. Something like:

public static Supplier<CsrfToken> createDefaultCsrfToken(String headerName, String parameterName) {
    return () -> new DefaultCsrfToken(headerName, paraemterName, createTokenValue());
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you suggest we modify CookieCsrfTokenRepository.loadToken() ?

@Override
public CsrfToken loadToken(HttpServletRequest request) {
	Cookie cookie = WebUtils.getCookie(request, this.cookieName);
	if (cookie == null) {
		return null;
	}
	String token = cookie.getValue();
	if (!StringUtils.hasLength(token)) {
		return null;
	}
	return this.generateTokenProvider.apply(token);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a good point. At this point I think it makes sense to change the API to be a Converter<String,CsrfToken> that allows passing in the csrf token string and the output is the CsrfToken implementation. When generating a new instance, the CsrfTokenRepository is in charge of generating the token and passing it into the Converter<String,CsrfToken>.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean org.springframework.core.convert.converter.Converter<S,T> ?

So it would be like below?

CookieCsrfTokenRepository repo = new CookieCsrfTokenRepository();
repo.setGenerateToken((tokenStrValue) -> new DefaultCsrfToken("XSRF-TOKEN", "_csrf", tokenStrValue));

if so this means that setHeaderName and setParameterName will not work at all

CookieCsrfTokenRepository repo = new CookieCsrfTokenRepository();
repo.setGenerateToken((tokenStrValue) -> new DefaultCsrfToken("XSRF-TOKEN", "_csrf", tokenStrValue));

repo.setParameterName("customParam"); // the token generated won't follow this value
repo.setHeaderName("customHeader"); // the token generated won't follow this value

and another issue is that users will be forced to maintain/hardcode default value of parameterName and headerName which were defined in both CookieCsrfTokenRepository and HttpSessionCsrfTokenRepository with default values _csrf and X-CSRF-TOKEN.

As for the current approach, it still consider setHeaderName and setParameterName from CookieCsrfTokenRepository

CookieCsrfTokenRepository repo = new CookieCsrfTokenRepository();
repo.setGenerateToken((headerName, parameterName, tokenStrValue) -> new DefaultCsrfToken(headerName, parameterName, tokenStrValue));

repo.setParameterName("customParam"); // the token generated will follow this value
repo.setHeaderName("customHeader"); // the token generated will follow this value

For the current approach, if users decides to maintain their own headerName and parameterName it could be done as per below

CookieCsrfTokenRepository repo = new CookieCsrfTokenRepository();
repo.setGenerateToken((headerName, parameterName, tokenStrValue) -> new DefaultCsrfToken("customHeader", "customParameter", tokenStrValue));

repo.setParameterName("customParam2"); // the token generated will NOT follow this value
repo.setHeaderName("customHeader2"); // the token generated will NOT follow this value


/**
* Generate CsrfToken from parameters
*
* @param headerName header name
* @param parameterName parameter name
* @param value token value
* @return CsrfToken generated from parameters
*/
T generateToken(String headerName, String parameterName, String value);
}
Loading