-
Notifications
You must be signed in to change notification settings - Fork 1
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
Add secret-less authentication for CI jobs. #131
Conversation
726b921
to
ca88e5c
Compare
7ebb0d2
to
5e3187e
Compare
37f8836
to
7381cec
Compare
e5d4e31
to
3703e6e
Compare
b587ff3
to
ca82b18
Compare
val tokenDuration: Duration? = null, | ||
) | ||
|
||
@ConfigurationProperties(prefix = "auth.external-jwt") |
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.
This deviates from our current approach we've been using in AppConfig
. I needed to do this because of the more complex structure the policies has (it is a list of a class which includes a map and a list as sub-fields).
Eventually I think it would actually be nice to move all configuration to this way of doing.
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.
do we need to add these into the applcicaton.properties? like the ones described in auth.md docs as well?
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.
@ConfigurationProperties(prefix = "auth.external-jwt") | |
@ConfigurationProperties(prefix = "auth.externalJwt") |
as per docs? or does it hyphen the camelcase?
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.
do we need to add these into the applcicaton.properties
Because these are much more structured, I don't think it makes sense to use flat string environment variables to pass them in, but instead need to pass them as structured data.
There's a number of ways we can do it, see: https://docs.spring.io/spring-boot/reference/features/external-config.html
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.
as per docs? or does it hyphen the camelcase?
So Spring is kind of weird and I got errors here if I didn't use kebab case here. It seems to apply some normalization to the input properties, which means that it doesn't actually matter what case people use, and both external-jwt
and externalJwt
would work.
See https://github.com/spring-projects/spring-boot/wiki/Canonical-properties
And https://github.com/spring-projects/spring-boot/blob/eb7b6a776dd8b01849a0ea35d097b81f7eeec23e/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/InvalidConfigurationPropertyNameFailureAnalyzer.java#L51-L52
Although you are right the docs I've written are inconsistent and use both. I'll switch to just one.
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.
I don't think it makes sense to use flat string environment variables to pass them in, but instead need to pass them as structured data.
so yeah I thought this structured data is picked up from the application.properties? because how does it know what to set the values to ? i thought you need what you have in your docs as well?
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.
It does get populated from the properties in the Environment
, but these can come from any number of sources, not just the built-inapplication.properties
.
See https://docs.spring.io/spring-boot/reference/features/external-config.html
The simplest way of doing it is probably through the command line arguments.
api/app/src/main/kotlin/packit/security/provider/TokenProvider.kt
Outdated
Show resolved
Hide resolved
The naming for this in other projects is kind of a mess:
I decided to avoid the OIDC name and just stick to JWT, or "External JWT" when it is ambiguous wrt Packit's own tokens. I'm happy to use a more catchy name if we want |
This is an example of it being used in practice: https://github.com/plietar/orderly-ci-test/blob/f1987986d5fceeaf7b4cceb9aacf2708ae776f0a/run.R |
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.
Nice work!!!! looks great and i learn lots just reading it!! just left some comments about code structure
val tokenDuration: Duration? = null, | ||
) | ||
|
||
@ConfigurationProperties(prefix = "auth.external-jwt") |
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.
do we need to add these into the applcicaton.properties? like the ones described in auth.md docs as well?
val tokenDuration: Duration? = null, | ||
) | ||
|
||
@ConfigurationProperties(prefix = "auth.external-jwt") |
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.
@ConfigurationProperties(prefix = "auth.external-jwt") | |
@ConfigurationProperties(prefix = "auth.externalJwt") |
as per docs? or does it hyphen the camelcase?
@ConfigurationProperties(prefix = "auth.external-jwt") | ||
data class JwtLoginConfig( | ||
val audience: String?, | ||
val policy: List<JwtPolicy> = listOf() |
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.
policies instead of policy?
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.
Done
|
||
val audience: String? = jwtLoginConfig.audience | ||
|
||
init { |
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.
all the init should probs be in JwtLoginConfig
and then it has the policies it can pass.. and thus we can keep the service clean
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.
I'm not sure I agree. The JwtLoginConfig
is a plain data class that just represents the properties. It seems odd that it would be initializing some business logic by creating validators and JWT decoders.
On the other hand I don't know if this type of init {}
blocks is common or the right way to do this.
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.
yeah that is fair but it seems like it does derive directly from config and doesn't need anything else right? and yeah the init in the service seems a bit odd.. maybe could create a bean with this logic and then directly inject into jwtlogin service?
something like this maybe?
@Configuration
class JwtConfig {
@Bean
fun tokenPolicies(jwtLoginConfig: JwtLoginConfig, restOperations: RestOperations): List<TokenPolicy> {
// init logic
}
}
then can pass into service straight up
class JwtLoginService(
val policies: List<TokenPolicy>,
val jwtIssuer: JwtIssuer,
val userService: UserService,
)
but if ya disagree feel free to keep your way 😄
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.
I think I'll keep it this way. The contents of the TokenPolicy
is so tightly coupled with the implementation of the service it doesn't really make sense to move it elsewhere. The only way the decoupling makes sense IMO is if we make the TokenPolicy an interface and move all the logic into it, at which point the service is nothing much more than a for-loop. Seems like an unnecessary abstraction, especially given there's only one kind of TokenPolicy.
I've changed it from a lateinit var
to a val
though, since I realised that works just as well.
@@ -23,23 +39,59 @@ class TokenProvider(val config: AppConfig) : JwtIssuer | |||
const val TOKEN_AUDIENCE = "packit" | |||
} | |||
|
|||
override fun issue(userPrincipal: UserPrincipal): String | |||
{ | |||
inner class Builder(val userPrincipal: UserPrincipal) : JwtBuilder { |
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.
the inner class stuff makes a bit messy... could move inner class outside and just pass along the config when calling instantiating class
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.
Done
override fun issue(userPrincipal: UserPrincipal): String | ||
{ | ||
inner class Builder(val userPrincipal: UserPrincipal) : JwtBuilder { | ||
var duration: Duration = Duration.of(config.authExpiryDays, ChronoUnit.DAYS) |
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.
should probs be private
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.
Done
} | ||
|
||
private fun issueToken(verifiedToken: Jwt, policy: JwtPolicy): String { | ||
val user = userService.getServiceUser() |
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.
also this all seems coupled to a service account... thus would a better name for all files and called be ServiceLogin...
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.
Yeah makes sense
@@ -50,6 +52,31 @@ class LoginController( | |||
return ResponseEntity.ok(token) | |||
} | |||
|
|||
@PostMapping("/login/jwt") |
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.
as per other comment about naming.. maybe should be /login/service
This change allows automated environments, such as Github Actions or Buildkite to authenticate to the Packit API without needing to be manually provisioned with a secret. The client's environment creates and signs a JWT containing claims describing the job. For example, on Github Actions, GitHub provides a token which includes the organisation name, repository name, type of event, branch name, etc. Packit needs to be configured to trust the 3rd party JWT issuer, with policies specifying what claims are needed. The client sends the JWT it received from its environment to the Packit `/auth/login/jwt` endpoint, and in exchange receives a Packit JWT which may be used to access the rest of the API.
1430ec1
to
20c80b5
Compare
20c80b5
to
d350824
Compare
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #131 +/- ##
=======================================
Coverage 97.22% 97.22%
=======================================
Files 131 131
Lines 1225 1225
Branches 339 339
=======================================
Hits 1191 1191
Misses 33 33
Partials 1 1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
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.
Nice work looks good to me!!! just a couple small comments but feel free to merge after response
Co-authored-by: Anmol Thapar <mr.anmolthapar@gmail.com>
This change allows automated environments, such as Github Actions or Buildkite to authenticate to the Packit API without needing to be manually provisioned with a secret.
The client's environment creates and signs a JWT containing claims describing the job. For example, on Github Actions, GitHub provides a token which includes the organisation name, repository name, type of event, branch name, etc. Packit needs to be configured to trust the 3rd party JWT issuer, with policies specifying what claims are needed.
The client sends the JWT it received from its environment to the Packit
/auth/login/jwt
endpoint, and in exchange receives a Packit JWT which may be used to access the rest of the API.