-
Notifications
You must be signed in to change notification settings - Fork 3.9k
[xDS] A97 - JWT token file call creds #12242
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/* | ||
* Copyright 2025 The gRPC 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.grpc.alts; | ||
|
||
import com.google.api.client.json.gson.GsonFactory; | ||
import com.google.api.client.json.webtoken.JsonWebSignature; | ||
import com.google.auth.oauth2.AccessToken; | ||
import com.google.auth.oauth2.OAuth2Credentials; | ||
import com.google.common.io.Files; | ||
import io.grpc.CallCredentials; | ||
import io.grpc.auth.MoreCallCredentials; | ||
import java.io.File; | ||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.Date; | ||
|
||
/** | ||
* JWT token file call credentials. | ||
* See gRFC A97 (https://github.com/grpc/proposal/pull/492). | ||
*/ | ||
public final class JwtTokenFileCallCredentials extends OAuth2Credentials { | ||
private static final long serialVersionUID = 452556614608513984L; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI: You can just use the value |
||
private String path = null; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
private JwtTokenFileCallCredentials(String path) { | ||
this.path = path; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Any time we save a value for later, we should be checking for null if it shouldn't be null. That means it is more likely the stack trace will tell us who provided the wrong value. The |
||
} | ||
|
||
@Override | ||
public AccessToken refreshAccessToken() throws IOException { | ||
String tokenString = new String(Files.toByteArray(new File(path)), StandardCharsets.UTF_8); | ||
Long expTime = JsonWebSignature.parse(new GsonFactory(), tokenString) | ||
.getPayload() | ||
.getExpirationTimeSeconds(); | ||
if (expTime == null) { | ||
throw new IOException("No expiration time found for JWT token"); | ||
} | ||
|
||
return AccessToken.newBuilder() | ||
.setTokenValue(tokenString) | ||
.setExpirationTime(new Date(expTime * 1000L)) | ||
.build(); | ||
} | ||
|
||
// using {@link MoreCallCredentials} adapter to be compatible with {@link CallCredentials} iface | ||
public static CallCredentials create(String path) { | ||
JwtTokenFileCallCredentials jwtTokenFileCallCredentials = new JwtTokenFileCallCredentials(path); | ||
return MoreCallCredentials.from(jwtTokenFileCallCredentials); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
/* | ||
* Copyright 2025 The gRPC 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.grpc.alts; | ||
|
||
import static org.junit.Assert.assertEquals; | ||
import static org.junit.Assert.assertThrows; | ||
|
||
import com.google.auth.oauth2.AccessToken; | ||
import com.google.common.truth.Truth; | ||
import java.io.File; | ||
import java.io.IOException; | ||
import java.lang.reflect.Constructor; | ||
import java.nio.charset.StandardCharsets; | ||
import java.nio.file.Files; | ||
import java.time.Instant; | ||
import java.util.Date; | ||
import java.util.concurrent.TimeUnit; | ||
import org.junit.Before; | ||
import org.junit.Rule; | ||
import org.junit.Test; | ||
import org.junit.experimental.runners.Enclosed; | ||
import org.junit.rules.TemporaryFolder; | ||
import org.junit.runner.RunWith; | ||
import org.junit.runners.JUnit4; | ||
|
||
/** Unit tests for {@link JwtTokenFileCallCredentials}. */ | ||
@RunWith(Enclosed.class) | ||
public class JwtTokenFileCallCredentialsTest { | ||
@RunWith(JUnit4.class) | ||
public static class WithEmptyJwtTokenTest { | ||
@Rule | ||
public TemporaryFolder tempFolder = new TemporaryFolder(); | ||
|
||
private File jwtTokenFile; | ||
private JwtTokenFileCallCredentials unit; | ||
|
||
@Before | ||
public void setUp() throws Exception { | ||
this.jwtTokenFile = JwtTokenFileTestUtils.createEmptyJwtToken(tempFolder); | ||
|
||
Constructor<JwtTokenFileCallCredentials> ctor = | ||
JwtTokenFileCallCredentials.class.getDeclaredConstructor(String.class); | ||
ctor.setAccessible(true); | ||
this.unit = ctor.newInstance(jwtTokenFile.toString()); | ||
} | ||
|
||
@Test | ||
public void givenJwtTokenFileEmpty_WhenTokenRefreshed_ExpectException() { | ||
assertThrows(IllegalArgumentException.class, () -> { | ||
unit.refreshAccessToken(); | ||
}); | ||
} | ||
} | ||
|
||
@RunWith(JUnit4.class) | ||
public static class WithInvalidJwtTokenTest { | ||
@Rule | ||
public TemporaryFolder tempFolder = new TemporaryFolder(); | ||
|
||
private File jwtTokenFile; | ||
private JwtTokenFileCallCredentials unit; | ||
|
||
@Before | ||
public void setUp() throws Exception { | ||
this.jwtTokenFile = JwtTokenFileTestUtils.createJwtTokenWithoutExpiration(tempFolder); | ||
|
||
Constructor<JwtTokenFileCallCredentials> ctor = | ||
JwtTokenFileCallCredentials.class.getDeclaredConstructor(String.class); | ||
ctor.setAccessible(true); | ||
this.unit = ctor.newInstance(jwtTokenFile.toString()); | ||
} | ||
|
||
@Test | ||
public void givenJwtTokenFileWithoutExpiration_WhenTokenRefreshed_ExpectException() | ||
throws Exception { | ||
Exception ex = assertThrows(IOException.class, () -> { | ||
unit.refreshAccessToken(); | ||
}); | ||
|
||
String expectedMsg = "No expiration time found for JWT token"; | ||
String actualMsg = ex.getMessage(); | ||
|
||
assertEquals(expectedMsg, actualMsg); | ||
} | ||
} | ||
|
||
@RunWith(JUnit4.class) | ||
public static class WithValidJwtTokenTest { | ||
@Rule | ||
public TemporaryFolder tempFolder = new TemporaryFolder(); | ||
|
||
private File jwtTokenFile; | ||
private JwtTokenFileCallCredentials unit; | ||
private Long givenExpTimeInSeconds; | ||
|
||
@Before | ||
public void setUp() throws Exception { | ||
this.givenExpTimeInSeconds = Instant.now().getEpochSecond() + TimeUnit.HOURS.toSeconds(1); | ||
|
||
this.jwtTokenFile = JwtTokenFileTestUtils.createValidJwtToken( | ||
tempFolder, givenExpTimeInSeconds); | ||
|
||
Constructor<JwtTokenFileCallCredentials> ctor = | ||
JwtTokenFileCallCredentials.class.getDeclaredConstructor(String.class); | ||
ctor.setAccessible(true); | ||
this.unit = ctor.newInstance(jwtTokenFile.toString()); | ||
} | ||
|
||
@Test | ||
public void givenValidJwtTokenFile_WhenTokenRefreshed_ExpectAccessTokenInstance() | ||
throws Exception { | ||
final Date givenExpTimeDate = new Date(TimeUnit.SECONDS.toMillis(givenExpTimeInSeconds)); | ||
|
||
String givenTokenValue = new String( | ||
Files.readAllBytes(jwtTokenFile.toPath()), | ||
StandardCharsets.UTF_8); | ||
|
||
AccessToken token = unit.refreshAccessToken(); | ||
|
||
Truth.assertThat(token.getExpirationTime()) | ||
.isEquivalentAccordingToCompareTo(givenExpTimeDate); | ||
assertEquals(token.getTokenValue(), givenTokenValue); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* | ||
* Copyright 2025 The gRPC 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.grpc.alts; | ||
|
||
import com.google.common.io.BaseEncoding; | ||
import java.io.File; | ||
import java.io.FileOutputStream; | ||
import java.nio.charset.StandardCharsets; | ||
import org.junit.rules.TemporaryFolder; | ||
|
||
public class JwtTokenFileTestUtils { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are only used by one test, and I don't expect many other tests to use this. We'd typically just have these as If you are going to keep this as a separate file, you probably should remove the TemporaryFolder usages in the class. That is mixing responsibilities and reduces the reusability. |
||
public static File createEmptyJwtToken(TemporaryFolder tempFolder) throws Exception { | ||
File jwtToken = tempFolder.newFile(new String("jwt.token")); | ||
return jwtToken; | ||
} | ||
|
||
public static File createJwtTokenWithoutExpiration(TemporaryFolder tempFolder) throws Exception { | ||
File jwtToken = tempFolder.newFile(new String("jwt.token")); | ||
FileOutputStream outputStream = new FileOutputStream(jwtToken); | ||
String content = | ||
BaseEncoding.base64().encode( | ||
new String("{\"typ\": \"JWT\", \"alg\": \"HS256\"}").getBytes(StandardCharsets.UTF_8)) | ||
+ "." | ||
+ BaseEncoding.base64().encode( | ||
new String("{\"name\": \"Google\"}").getBytes(StandardCharsets.UTF_8)) | ||
+ "." | ||
+ BaseEncoding.base64().encode(new String("signature").getBytes(StandardCharsets.UTF_8)); | ||
outputStream.write(content.getBytes(StandardCharsets.UTF_8)); | ||
outputStream.close(); | ||
return jwtToken; | ||
} | ||
|
||
public static File createValidJwtToken(TemporaryFolder tempFolder, Long expTime) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/Long/long/. It doesn't look like |
||
throws Exception { | ||
File jwtToken = tempFolder.newFile(new String("jwt.token")); | ||
FileOutputStream outputStream = new FileOutputStream(jwtToken); | ||
String content = | ||
BaseEncoding.base64().encode( | ||
new String("{\"typ\": \"JWT\", \"alg\": \"HS256\"}").getBytes(StandardCharsets.UTF_8)) | ||
+ "." | ||
+ BaseEncoding.base64().encode( | ||
String.format("{\"exp\": %d}", expTime).getBytes(StandardCharsets.UTF_8)) | ||
+ "." | ||
+ BaseEncoding.base64().encode(new String("signature").getBytes(StandardCharsets.UTF_8)); | ||
outputStream.write(content.getBytes(StandardCharsets.UTF_8)); | ||
outputStream.close(); | ||
return jwtToken; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why were these files added to
alts
? I'd have expectedxds
.