Return GRPC statuses for more Okta errors. - #200
Conversation
| // Fall back to http status code. | ||
| if resp != nil && (resp.StatusCode < 200 || resp.StatusCode >= 300) { | ||
| grpcCode := uhttp.GrpcCodeFromHTTPStatus(resp.StatusCode) | ||
| return uhttp.WrapErrorsWithRateLimitInfo(grpcCode, resp.Response, err) | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion (medium confidence): this HTTP-status fallback broadens codes.NotFound from "Okta returned E0000007/E0000008" to "any 404 response". ensureUserDeactivated, waitForUserDeprovisioned, and permanentlyDeleteUser (user.go:873/888/913/964/972) all treat codes.NotFound as proof the user is absent and return success, so a 404 that is not a genuine user-not-found — misrouted path, wrong org URL, or a proxy/WAF 404 with a non-JSON body (okta's CheckResponseForError still yields an *okta.Error with an empty ErrorCode) — now makes Delete() report a successful deprovision without anything being deleted. Consider restricting the not-found→absent inference in user.go to the Okta error codes, or only falling back to NotFound when the body actually parsed as an Okta error.
| var oktaApiError *okta.Error | ||
| if errors.As(err, &oktaApiError) { | ||
| grpcErrCode, ok := oktaErrToGRPCError[oktaApiError.ErrorCode] | ||
| if ok { | ||
| return errors.Join(status.Error(grpcErrCode, oktaApiError.ErrorSummary), err) | ||
| } | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion (low confidence): the Okta error-code lookup now wins over the HTTP status, which removes the old unconditional resp.StatusCode >= 500 → codes.Unavailable. helpers_test.go:66-71 encodes this deliberately (500 + E0000007 → NotFound), so a 5xx that happens to carry one of the four mapped codes becomes non-retryable. Okta's real 5xx bodies use E0000009, so impact is low, but checking resp.StatusCode >= 500 before the code map would preserve retryability without changing any of the 4xx behavior this PR is adding.
There was a problem hiding this comment.
If the okta code is unknown to us, we fall back to using the http response, which maps 5xx to unavailable, which is a retryable error.
Connector PR Review: Return GRPC statuses for more Okta errors.Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0 Review SummaryScanned the full PR diff for security and correctness: Security IssuesNone found. Correctness IssuesNone found. Suggestions
Prompt for AI agents |
b7b4df4 to
b4b351b
Compare
| return nil, resp, err | ||
| } | ||
|
|
||
| return resourceSetBindings.Members, resp, nil |
There was a problem hiding this comment.
🟡 Suggestion: resourceSetBindings stays nil when the request succeeds with an empty or null body — the Okta SDK's buildResponse returns early without decoding when len(bodyBytes) == 0, so resourceSetBindings.Members panics. Since this function is already being touched for error plumbing, a if resourceSetBindings == nil { return nil, resp, nil } guard would close that path (resource_sets.go:183 has the same shape with .Roles). Pre-existing, medium confidence — non-blocking.
b4b351b to
3d8bc51
Compare
| run: go build ./cmd/baton-okta | ||
| - name: Test grant/revoke | ||
| uses: ConductorOne/github-workflows/actions/sync-test@v4 | ||
| uses: ConductorOne/github-workflows/actions/sync-test@c921d6cc32adab244b17a68109edacd884271557 |
There was a problem hiding this comment.
🟡 Suggestion: c921d6cc32adab244b17a68109edacd884271557 is not on ConductorOne/github-workflows main and is not contained in the v4 tag (it compares as 1 ahead of both), so CI now runs an unmerged workflow revision that will never pick up future v4 updates. Consider merging that change and reverting these two uses: lines back to @v4 once the tag includes it.
| resp, err := doRequest(ctx, reqUrl.String(), http.MethodGet, &resourceSetBindings, client) | ||
| if err != nil { | ||
| return nil, nil, err | ||
| return nil, resp, err |
There was a problem hiding this comment.
🟡 Suggestion: the newly-returned resp is only consumed by Grants (line 243). Revoke (line 352) still calls listMembersOfBinding with _ for the response and returns the bare error, and both Grant (line 318) and Revoke (line 373) format the underlying error with %s/err.Error(), which flattens the chain. Net effect: resource-set-binding provisioning failures still surface without a gRPC status, which is the behavior this PR is adding elsewhere. Threading resp through handleOktaResponseError and switching those two to %w would make provisioning consistent with the sync path.
| applicationUsers, respCtx, err := listApplicationUsers(ctx, o.client, resource.Id.GetResource(), token, qp) | ||
| if err != nil { | ||
| return nil, nil, bag, convertNotFoundError(err, "okta-connectorv2: failed to list group users") | ||
| return nil, nil, bag, fmt.Errorf("okta-connectorv2: failed to list app users for app %s: %w", resource.Id.GetResource(), err) |
There was a problem hiding this comment.
🟡 Suggestion: listApplicationUsers (app.go:288) already wraps this error as okta-connectorv2: failed to fetch app users from okta: …, so the message becomes doubly prefixed. It is also inconsistent with the sibling listAppGroupGrants path just above (line 176), which now returns bare err. Same pattern at group.go:149, where listGroupUsers (group.go:318) already prefixes. Picking one convention would keep the messages readable.
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| got := handleOktaResponseError(tt.resp, tt.err) |
There was a problem hiding this comment.
🟡 Suggestion: every call site wraps the result in fmt.Errorf("…: %w", handleOktaResponseError(...)), and user.go:873/888/913/964/972 then rely on status.Code(err) == codes.NotFound resolving through both that wrapper and the errors.Join inside this function. The table asserts on the unwrapped return only, so a future change to the wrapping (e.g. %v) would silently break deprovision/delete idempotency without failing a test. Consider one case that asserts the code survives fmt.Errorf("x: %w", got).
3d8bc51 to
040f15a
Compare
| } | ||
|
|
||
| roles, respCtx, err := listBindings(ctx, rs.client, resource.Id.Resource) | ||
| roles, oktaResp, err := listBindings(ctx, rs.client, resource.Id.Resource) |
There was a problem hiding this comment.
🟡 Suggestion (medium confidence): listBindings (line 183) dereferences resourceSetsBindings.Roles while the variable stays nil whenever the request succeeds with an empty or null body — the Okta SDK's buildResponse returns early without decoding when len(bodyBytes) == 0, so this Grants call panics instead of returning zero grants. Same pattern already flagged at resource_sets_bindings.go:166; guard both with if resourceSetsBindings == nil { return nil, resp, nil }.
| return nil, nil, err | ||
| } | ||
| default: | ||
| return nil, nil, fmt.Errorf("okta-connectorv2: unexpected resource for app: %w", err) |
There was a problem hiding this comment.
🟡 Suggestion: err is guaranteed nil here — it was set by parsePageToken on line 129 and returned on line 130, and the default arm is reached without any intervening assignment. So %w formats as %!w(<nil>) and the resulting error carries no cause and no gRPC status. Since this PR is specifically about error classification, consider status.Errorf(codes.Internal, "okta-connectorv2: unexpected app grant type %q", bag.ResourceID()).
040f15a to
768a41c
Compare
768a41c to
dba68b4
Compare
| return nil, &sdkResource.SyncOpResults{NextPageToken: pageToken}, nil | ||
| } else { | ||
| return nil, nil, convertNotFoundError(&errOkta, "okta-connectorv2: failed to list group roles") | ||
| return nil, nil, fmt.Errorf("okta-connectorv2: failed to list group roles: %w", handleOktaResponseError(resp, &errOkta)) |
There was a problem hiding this comment.
🟡 Suggestion (medium confidence): this line now routes the parsed Okta error through handleOktaResponseError, but the getError failure path just above (line 190, return nil, nil, err) still escapes uncoded. getError fails whenever the error body is not JSON — proxy/gateway 5xx HTML, empty body — and on 401/403 responses carrying Www-Authenticate: Bearer the Okta SDK returns the error without decoding the body at all, so json.Unmarshal on it fails too. Those cases surface as a bare unexpected end of JSON input with codes.Unknown instead of the Unavailable/Unauthenticated this PR aims to produce; wrapping line 190 as handleOktaResponseError(resp, err) would close the gap.
Okta answers a bad DPoP proof with a 400, a zero-length body, and the reason in a DPoP-scheme WWW-Authenticate header. The vendored SDK's CheckResponseForError reads that header only for 401/403 responses whose scheme is Bearer, then discards the decode failure on the empty body, so every such rejection reached the caller as "the API returned an unknown error" and failed the whole sync with no way to tell what happened. Confirmed live against a DPoP-enabled tenant. A replayed proof jti, an iat more than five minutes old, and an iat in the future all produce that exact shape; an unsolicited nonce is accepted, and no resource response ever offers a DPoP-Nonce, so the resource server issues no nonce challenges at all. oktaauth: - rewrite an empty-bodied rejection's body from the WWW-Authenticate params so the SDK renders Okta's actual reason - retry once with a fresh proof on proof replay specifically; uhttp's transport retries with headers untouched, so a stale-connection retry can resend the same jti. A generic invalid_dpop_proof is not retried, since a skewed clock would only triple the traffic - warn on any DPoP-related 4xx, logging the full challenge before the SDK discards it - match a nonce challenge on 400 as well as 401, and in the JSON-body shape as well as the header, with a bounded retry loop. Defensive: unreachable on the tenants observed, but correct per RFC 9449 and consistent with the token endpoint's existing handling connector: - getError now names the HTTP status and excerpts an undecodable body. Its five call sites all return its error verbatim, so an empty body surfaced as a bare "unexpected end of JSON input" with no status and no prefix. #200 rewrote handleOktaResponseError but left getError untouched. - the event feed no longer returns the SDK error bare, so it picks up a grpc code and the status line like every other call site. #200 did not touch event_log.go. Status codes and grpc codes for everything routed through handleOktaResponseError, including 429, come from #200's GrpcCodeFromHTTPStatus fallback; no duplicate handling is added here. Validated with three clean full syncs against a live tenant (119 actions, no errors) plus unit coverage of each path using the captured challenge strings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Okta answers a bad DPoP proof with a 400, a zero-length body, and the reason in a DPoP-scheme WWW-Authenticate header. The vendored SDK's CheckResponseForError reads that header only for 401/403 responses whose scheme is Bearer, then discards the decode failure on the empty body, so every such rejection reached the caller as "the API returned an unknown error" and failed the whole sync with no way to tell what happened. Confirmed live against a DPoP-enabled tenant. A replayed proof jti, an iat more than five minutes old, and an iat in the future all produce that exact shape; an unsolicited nonce is accepted, and no resource response ever offers a DPoP-Nonce, so the resource server issues no nonce challenges at all. oktaauth: - rewrite an empty-bodied rejection's body from the WWW-Authenticate params so the SDK renders Okta's actual reason. Scoped to client errors and to the DPoP challenge within the header: RFC 9110 permits several challenges in one value, and a body outside 4xx must never be replaced - retry once with a fresh proof on proof replay specifically; uhttp's transport retries with headers untouched, so a stale-connection retry can resend the same jti. A generic invalid_dpop_proof is not retried, since a skewed clock would only triple the traffic - log any DPoP-related 4xx at debug, with the full challenge, before the SDK discards it. Debug not warn: a skewed clock is not retried, so warn would fire for every request in a sync, and the reason now reaches the caller in the error itself - match a nonce challenge on 400 as well as 401, and in the JSON-body shape as well as the header, with a bounded retry loop. Defensive: unreachable on the tenants observed, but correct per RFC 9449 and consistent with the token endpoint's existing handling connector: - getError now names the HTTP status and excerpts an undecodable body, trimming to a rune boundary so the excerpt stays valid UTF-8. Its five call sites all return its error verbatim, so an empty body surfaced as a bare "unexpected end of JSON input" with no status and no prefix. #200 rewrote handleOktaResponseError but left getError untouched. - the event feed no longer returns the SDK error bare, so it picks up a grpc code and the status line like every other call site. #200 did not touch event_log.go. Status codes and grpc codes for everything routed through handleOktaResponseError, including 429, come from #200's GrpcCodeFromHTTPStatus fallback; no duplicate handling is added here. Validated with three clean full syncs against a live tenant (119 actions, no errors) plus unit coverage of each path using the captured challenge strings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Okta answers a bad DPoP proof with a 400, a zero-length body, and the reason in a DPoP-scheme WWW-Authenticate header. The vendored SDK's CheckResponseForError reads that header only for 401/403 responses whose scheme is Bearer, then discards the decode failure on the empty body, so every such rejection reached the caller as "the API returned an unknown error" and failed the whole sync with no way to tell what happened. Confirmed live against a DPoP-enabled tenant. A replayed proof jti, an iat more than five minutes old, and an iat in the future all produce that exact shape; an unsolicited nonce is accepted, and no resource response ever offers a DPoP-Nonce, so the resource server issues no nonce challenges at all. oktaauth: - restate the WWW-Authenticate challenge as the error body the SDK expects, so it renders what Okta actually said. The challenge is passed through whole rather than parsed into auth-params: RFC 9110 allows several challenges in one header, attributing a param to the right scheme needs a real parser, and quoting the header drops nothing and cannot misattribute one scheme's error to another. Bounded to client errors with an empty body, so a real body is never replaced. - retry once with a fresh proof on proof replay specifically; uhttp's transport retries with headers untouched, so a stale-connection retry can resend the same jti. A generic invalid_dpop_proof is not retried, since a skewed clock would only triple the traffic - log any DPoP-related 4xx at Warn with the full challenge, before the SDK discards it, logarithmically sampled with a total_occurrences field: a skewed clock is not retried, so an unsampled line would fire once per request for a whole sync - match a nonce challenge on 400 as well as 401, and in the JSON-body shape as well as the header, with a bounded retry loop. Defensive: unreachable on the tenants observed, but correct per RFC 9449 and consistent with the token endpoint's existing handling connector: - getError now names the HTTP status and excerpts an undecodable body, trimming to a rune boundary so the excerpt stays valid UTF-8. Its five call sites all return its error verbatim, so an empty body surfaced as a bare "unexpected end of JSON input" with no status and no prefix. #200 rewrote handleOktaResponseError but left getError untouched. - the event feed no longer returns the SDK error bare, so it picks up a grpc code and the status line like every other call site. #200 did not touch event_log.go. Status codes and grpc codes for everything routed through handleOktaResponseError, including 429, come from #200's GrpcCodeFromHTTPStatus fallback; no duplicate handling is added here. Validated with three clean full syncs against a live tenant (119 actions, no errors) plus unit coverage of each path using the captured challenge strings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Okta answers a bad DPoP proof with a 400, a zero-length body, and the reason in a DPoP-scheme WWW-Authenticate header. The vendored SDK's CheckResponseForError reads that header only for 401/403 responses whose scheme is Bearer, then discards the decode failure on the empty body, so every such rejection reached the caller as "the API returned an unknown error" and failed the whole sync with no way to tell what happened. Confirmed live against a DPoP-enabled tenant. A replayed proof jti, an iat more than five minutes old, and an iat in the future all produce that exact shape; an unsolicited nonce is accepted, and no resource response ever offers a DPoP-Nonce, so the resource server issues no nonce challenges at all. oktaauth: - restate the WWW-Authenticate challenge as the error body the SDK expects, so it renders what Okta actually said. The challenge is passed through whole rather than parsed into auth-params: RFC 9110 allows several challenges in one header, attributing a param to the right scheme needs a real parser, and quoting the header drops nothing and cannot misattribute one scheme's error to another. Bounded to client errors with an empty body, so a real body is never replaced. - retry once with a fresh proof when Okta reports a replayed proof jti; uhttp's transport retries a failed request with its headers untouched, so a stale-connection retry resends the same proof. Matched on the full DPoP-specific phrase, not on "already been used" alone, which another scheme's challenge in the same header could carry. A generic invalid_dpop_proof is not retried, since a skewed clock would only triple the traffic. - log any DPoP-related 4xx at Warn with the full challenge, before the SDK discards it, logarithmically sampled with a total_occurrences field: a skewed clock is not retried, so an unsampled line would fire once per request for a whole sync - guard the retry drain against a nil response body. net/http always sets one, but io.Copy on a nil ReadCloser panics and the two neighbouring helpers already check it. - match a nonce challenge on 400 as well as 401, and in the JSON-body shape as well as the header, with a bounded retry loop. Defensive: unreachable on the tenants observed, but correct per RFC 9449 and consistent with the token endpoint's existing handling connector: - getError now names the HTTP status and excerpts an undecodable body. Its five call sites all return its error verbatim, so an empty body surfaced as a bare "unexpected end of JSON input" with no status and no prefix. #200 rewrote handleOktaResponseError but left getError untouched. - the event feed no longer returns the SDK error bare, so it picks up a grpc code and the status line like every other call site. #200 did not touch event_log.go. Both truncation paths back up to a rune boundary rather than cutting at a byte offset, so an excerpt reaching an error message or a log field stays valid UTF-8. Status codes and grpc codes for everything routed through handleOktaResponseError, including 429, come from #200's GrpcCodeFromHTTPStatus fallback; no duplicate handling is added here. Validated with three clean full syncs against a live tenant (119 actions, no errors) plus unit coverage of each path using the captured challenge strings. The panic guard and both rune-boundary cuts are verified by mutation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Okta answers a bad DPoP proof with a 400, a zero-length body, and the reason in a DPoP-scheme WWW-Authenticate header. The vendored SDK's CheckResponseForError reads that header only for 401/403 responses whose scheme is Bearer, then discards the decode failure on the empty body, so every such rejection reached the caller as "the API returned an unknown error" and failed the whole sync with no way to tell what happened. Confirmed live against a DPoP-enabled tenant. A replayed proof jti, an iat more than five minutes old, and an iat in the future all produce that exact shape; an unsolicited nonce is accepted, and no resource response ever offers a DPoP-Nonce, so the resource server issues no nonce challenges at all. oktaauth: - restate the WWW-Authenticate challenge as the error body the SDK expects, so it renders what Okta actually said. The challenge is passed through whole rather than parsed into auth-params: RFC 9110 allows several challenges in one header, attributing a param to the right scheme needs a real parser, and quoting the header drops nothing and cannot misattribute one scheme's error to another. Bounded to client errors with a genuinely empty body, so a real body is never replaced -- including a body that failed partway through being read, whose bytes are reported rather than mistaken for an empty body. - retry once with a fresh proof when Okta reports a replayed proof jti; uhttp's transport retries a failed request with its headers untouched, so a stale-connection retry resends the same proof. Matched on the full DPoP-specific phrase, not on "already been used" alone, which another scheme's challenge in the same header could carry. A generic invalid_dpop_proof is not retried, since a skewed clock would only triple the traffic. - log any DPoP-related 4xx at Warn with the full challenge, before the SDK discards it, logarithmically sampled with a total_occurrences field: a skewed clock is not retried, so an unsampled line would fire once per request for a whole sync - guard the retry drain against a nil response body. net/http always sets one, but io.Copy on a nil ReadCloser panics and the two neighbouring helpers already check it. - match a nonce challenge on 400 as well as 401, and in the JSON-body shape as well as the header, with a bounded retry loop. Defensive: unreachable on the tenants observed, but correct per RFC 9449 and consistent with the token endpoint's existing handling connector: - getError now names the HTTP status, excerpts an undecodable body, and carries a grpc code derived from that status. Its five call sites return its error verbatim, so an empty body surfaced as a bare "unexpected end of JSON input" with no status and no prefix, and an unparseable body reached the sync as codes.Unknown while a parseable one became PermissionDenied for the same upstream failure. #200 rewrote handleOktaResponseError but left getError untouched. - the event feed no longer returns the SDK error bare, so it picks up a grpc code and the status line like every other call site. #200 did not touch event_log.go. Both truncation paths back up to a rune boundary rather than cutting at a byte offset, so an excerpt reaching an error message or a log field stays valid UTF-8. Status codes and grpc codes for everything routed through handleOktaResponseError, including 429, come from #200's GrpcCodeFromHTTPStatus fallback; no duplicate handling is added here. Validated with four clean full syncs against a live tenant (119 actions, no errors) plus unit coverage of each path using the captured challenge strings. The panic guard, the partial-read fix, and both rune-boundary cuts are verified by mutation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…02) (#202) * fix: surface DPoP proof rejections instead of "unknown error" (CXP-1002) Okta answers a bad DPoP proof with a 400, a zero-length body, and the reason in a DPoP-scheme WWW-Authenticate header. The vendored SDK's CheckResponseForError reads that header only for 401/403 responses whose scheme is Bearer, then discards the decode failure on the empty body, so every such rejection reached the caller as "the API returned an unknown error" and failed the whole sync with no way to tell what happened. Confirmed live against a DPoP-enabled tenant. A replayed proof jti, an iat more than five minutes old, and an iat in the future all produce that exact shape; an unsolicited nonce is accepted, and no resource response ever offers a DPoP-Nonce, so the resource server issues no nonce challenges at all. oktaauth: - restate the WWW-Authenticate challenge as the error body the SDK expects, so it renders what Okta actually said. The challenge is passed through whole rather than parsed into auth-params: RFC 9110 allows several challenges in one header, attributing a param to the right scheme needs a real parser, and quoting the header drops nothing and cannot misattribute one scheme's error to another. Bounded to client errors with a genuinely empty body, so a real body is never replaced -- including a body that failed partway through being read, whose bytes are reported rather than mistaken for an empty body. - retry once with a fresh proof when Okta reports a replayed proof jti; uhttp's transport retries a failed request with its headers untouched, so a stale-connection retry resends the same proof. Matched on the full DPoP-specific phrase, not on "already been used" alone, which another scheme's challenge in the same header could carry. A generic invalid_dpop_proof is not retried, since a skewed clock would only triple the traffic. - log any DPoP-related 4xx at Warn with the full challenge, before the SDK discards it, logarithmically sampled with a total_occurrences field: a skewed clock is not retried, so an unsampled line would fire once per request for a whole sync - guard the retry drain against a nil response body. net/http always sets one, but io.Copy on a nil ReadCloser panics and the two neighbouring helpers already check it. - match a nonce challenge on 400 as well as 401, and in the JSON-body shape as well as the header, with a bounded retry loop. Defensive: unreachable on the tenants observed, but correct per RFC 9449 and consistent with the token endpoint's existing handling connector: - getError now names the HTTP status, excerpts an undecodable body, and carries a grpc code derived from that status. Its five call sites return its error verbatim, so an empty body surfaced as a bare "unexpected end of JSON input" with no status and no prefix, and an unparseable body reached the sync as codes.Unknown while a parseable one became PermissionDenied for the same upstream failure. #200 rewrote handleOktaResponseError but left getError untouched. - the event feed no longer returns the SDK error bare, so it picks up a grpc code and the status line like every other call site. #200 did not touch event_log.go. Both truncation paths back up to a rune boundary rather than cutting at a byte offset, so an excerpt reaching an error message or a log field stays valid UTF-8. Status codes and grpc codes for everything routed through handleOktaResponseError, including 429, come from #200's GrpcCodeFromHTTPStatus fallback; no duplicate handling is added here. Validated with four clean full syncs against a live tenant (119 actions, no errors) plus unit coverage of each path using the captured challenge strings. The panic guard, the partial-read fix, and both rune-boundary cuts are verified by mutation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: cut the token-endpoint error excerpt on a rune boundary too formatTokenError excerpts an unparseable token-endpoint body the same way the resource path does, and had the same defect this branch fixes elsewhere: a byte slice at errorBodyExcerptLimit can split a multi-byte rune and leave invalid UTF-8 in the returned gRPC message. It sits in the same package as the truncateAtRuneBoundary helper added here, so the guard was being shipped with a live instance of the bug beside it. Point it at the helper and pin it with a test: restoring the byte slice fails with `desc = token endpoint 502 Bad Gateway: EUR...\xe2\x82...`. Reached whenever the body carries no error/error_description for parseTokenError to pick up -- a non-JSON page from a proxy, say -- which is the same likelihood as the resource-side case already covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
All auth errors were returning an unknown error instead of a GRPC status, causing us to misclassify them. This change adds some okta errors besides notfound, and falls back to the response's http status if that lookup fails.