Add app auth config sessions service, and implement create app session with JWT tokens#62324
Add app auth config sessions service, and implement create app session with JWT tokens#62324gabrielcorado merged 10 commits intomasterfrom
Conversation
| // SessionId is ID the new app session will use. | ||
| string session_id = 2; |
There was a problem hiding this comment.
it might be safer for auth to generate this instead of inputting it from proxy.
There was a problem hiding this comment.
Agreed. The thing is, we still require the proxy to generate the session ID (otherwise, we would need an auth server request for every MCP/APP request).
What about moving this logic into a shared package (maybe api/ or something else) and having the auth not generate it but validate it instead?
There was a problem hiding this comment.
personally i would prefer lib/services over api/ but either is fine. or a new common package.
that said, i wonder if we can do in a way that Proxy does not need to guess. we can brainstorm some ideas offline.
There was a problem hiding this comment.
I've moved this logic into a shared package that can be used by auth and proxy. So now, the auth will generate the session ID, and the proxy will also try to generate it when retrieving the session.
We can consider doing something different here later, like not using this hashed JWT as the session ID (use another field to store it) and making the proxy capable of searching by that field. For now, we'll keep it this way, and if we find out that this needs to change, we can update it (there are some backward compatibility requirements, but it's all internal, so we should be fine).
|
Sorry for the delay. Reviewing now. |
| // SessionId is ID the new app session will use. | ||
| string session_id = 2; |
| // ClientAddr is a client (user's) address. | ||
| string client_addr = 5; |
There was a problem hiding this comment.
Can we please be more specific about what this is? Is it the client source IP address?
There was a problem hiding this comment.
Yes, it is the client's remote IP address (LoginIP). I've renamed it to RemoteAddr to match the name used elsewhere.
There was a problem hiding this comment.
That is fine, but just please add IP so when we consume the string we know exactly what to parse it as
// RemoteAddr is a client (user's) IP address.
I know it seems like a minor detail, but in my networking mind "remote address" can sometimes contain the source port or can mean other things depending on the context
|
Reviewing now |
cthach
left a comment
There was a problem hiding this comment.
Looks good overall. Left mostly nit suggestions. Have one blocking comment regarding removing legacy dependency if possible since we're introducing a new service.
| // Response for CreateAppSessionWithJWT. | ||
| message CreateAppSessionWithJWTResponse { | ||
| // Session is the app session. | ||
| types.WebSessionV2 session = 1; |
There was a problem hiding this comment.
We had a ton of issues with gogoproto and have been trying to move away from it.
Is there anyway possible that we can avoid importing teleport/legacy/types/types.proto, even if that means having to define a new package with what we need?
If it is too much of a lift, I understand, but this would really help with our long-term transition away from gogoproto and legacy code 🙏🏾
There was a problem hiding this comment.
100% we should make this change. But I don't think this is the right PR to do it (given that we'll need to propose a new type, and ideally get feedback on it), and here is my suggestion:
- We go with this PR as it is (using legacy
types.WebSessionV2) and the following PR that will use this service (changes atlib/web/app). - I can open a separate PR that has no dependency on either of those two with the new app session type and helper/conversion functions for the legacy type. We can review it and make it ready for usage.
- After 2 is done, I can go back to this and update. We could assume this won't be released yet, so there are no issues with backward compatibility.
What do you think?
Also, @greedy52, for visibility on this.
There was a problem hiding this comment.
That sounds good to me. Thank you!!
cthach
left a comment
There was a problem hiding this comment.
Great work.
Please address the authz issue before merging.
| sid := services.GenerateAppSessionIDFromJWT(req.Jwt) | ||
| defer func() { | ||
| if emitErr := s.emitter.EmitAuditEvent(ctx, newVerifyJWTAuditEvent(ctx, req, "", err)); emitErr != nil { | ||
| s.logger.ErrorContext(ctx, "failed to emit jwt verification audit event", "error", emitErr) | ||
| } | ||
| }() | ||
|
|
||
| if err := validateCreateAppSessionWithJWTRequest(req); err != nil { | ||
| return nil, trace.Wrap(err) | ||
| } | ||
|
|
||
| authCtx, err := s.authorizer.Authorize(ctx) | ||
| if err != nil { | ||
| return nil, trace.Wrap(err) | ||
| } | ||
|
|
||
| if !authz.HasBuiltinRole(*authCtx, string(types.RoleProxy)) { | ||
| return nil, trace.AccessDenied("this request can be only executed by a proxy") | ||
| } |
There was a problem hiding this comment.
| sid := services.GenerateAppSessionIDFromJWT(req.Jwt) | |
| defer func() { | |
| if emitErr := s.emitter.EmitAuditEvent(ctx, newVerifyJWTAuditEvent(ctx, req, "", err)); emitErr != nil { | |
| s.logger.ErrorContext(ctx, "failed to emit jwt verification audit event", "error", emitErr) | |
| } | |
| }() | |
| if err := validateCreateAppSessionWithJWTRequest(req); err != nil { | |
| return nil, trace.Wrap(err) | |
| } | |
| authCtx, err := s.authorizer.Authorize(ctx) | |
| if err != nil { | |
| return nil, trace.Wrap(err) | |
| } | |
| if !authz.HasBuiltinRole(*authCtx, string(types.RoleProxy)) { | |
| return nil, trace.AccessDenied("this request can be only executed by a proxy") | |
| } | |
| authCtx, err := s.authorizer.Authorize(ctx) | |
| if err != nil { | |
| return nil, trace.Wrap(err) | |
| } | |
| if !authz.HasBuiltinRole(*authCtx, string(types.RoleProxy)) { | |
| return nil, trace.AccessDenied("this request can be only executed by a proxy") | |
| } | |
| sid := services.GenerateAppSessionIDFromJWT(req.Jwt) | |
| defer func() { | |
| if emitErr := s.emitter.EmitAuditEvent(ctx, newVerifyJWTAuditEvent(ctx, req, "", err)); emitErr != nil { | |
| s.logger.ErrorContext(ctx, "failed to emit jwt verification audit event", "error", emitErr) | |
| } | |
| }() | |
| if err := validateCreateAppSessionWithJWTRequest(req); err != nil { | |
| return nil, trace.Wrap(err) | |
| } |
Shouldn't we do our authz checks first?
Sorry, I missed this in the first pass.
There was a problem hiding this comment.
Nice catch. I've moved the authz to the top, but I kept the defer function before, so we can still generate audit logs when authz fails as well.
There was a problem hiding this comment.
generate audit logs when authz fails as well
I'm a relatively new dev at Teleport. Is this a common pattern?
Personally I would be concerned about malicious unauthorized users spamming this endpoint, possibly causing a DoS via the side effect that each failure, even from unauthorized user, creates an audit event and thereby consuming resources.
Then again, I'm especially paranoid. If this potential attack vector is something we accepted in our threat model, I'm good with it, just wanted us to acknowledge.
There was a problem hiding this comment.
I'm a relatively new dev at Teleport. Is this a common pattern?
(I had to double-check on this) It seems we only log access denied for some resource access. For example, if you try to access a database using a database user you don't have access to. With that said, I'll move the emit to occur after authz, as there is not much value in having it (see below for more).
Personally I would be concerned about malicious unauthorized users spamming this endpoint, possibly causing a DoS via the side effect that each failure, even from unauthorized user, creates an audit event and thereby consuming resources.
Only issuing an audit after the authorization is performed won't affect the concern you raised, since it only verifies that the request was made by an internal component (Proxy in this case). No user permissions are currently verified at this step. The user's authorization to access X occurs later, when we generate the session, and already generates an audit event.
To reduce the risk, as you said, we'll impose a rate limit on callers to this method (which will be publicly accessible and requested by users). I couldn't find any place where we enforce this measure on the auth server methods.
There was a problem hiding this comment.
Thanks for hearing me out.
I forgot that there was an authentication check prior to this logic. I was mostly concerned with us triggering events for unauthenticated and unauthorized requests.
I chatted with @rosstimothy and he mentioned that we have started to move towards emitting audit events for successful and failed authorizations. For example, see #62384.
So I believe what you had earlier where we emit the event even on failed authorization was good and inline with this pattern.
Apologies for the back and forth!
…uthz is performed"
Related to RFD 0030e.
Brief context for reviewers on RFD 0030e: It allows Teleport to verify JWT tokens issued by external services (such as an IdP) with an audience set to Teleport (as specified by configuration). To enable this, we need to start sessions based on these JWT tokens, and this sessions service handles that process.
As per RFD, this service is responsible for receiving the JWT (external) sent and performing:
Note: This service will be used by Teleport Proxy to generate the app sessions. This will be done in a separate PR and will include integration tests.