Correct API pattern for different security mechanisms depending on internal or external consumption

Hello,

I am looking for advice on the correct API pattern to follow when an API needs to have different security mechanisms applied depending on whether it's getting used for Internal apps, or External users.

  • Internal use - The app only needs to be authorized (e.g. OAuth2), no user authentication is needed
  • External use - The app needs to be authorized, and the user needs to be authenticated (e.g. OpenID Connect).

One possibility is to have 2 x separate API proxies; one for internal with OAuth2, and one for external with OIDC.

However, this could lead to the duplication of many APIs in future as there will be lots of future use-cases like this.

The ideal-state would be logic in 1 API proxy that would authenticate/authorize depending on the user. Perhaps API Products could be leveraged using specific paths; a Product for internals with the Oauth path only, and a Product for externals with OIDC path. Although in that case there may need to be further logic within the proxy to flag internal/external use and what the user can do/see/change through the API proxy.

Solved Solved
0 2 235
1 ACCEPTED SOLUTION

I understand the words you're saying, but I have a different view.  I don't think it is a wise idea to expose APIs to sometimes require user authentication, and sometimes not. That seems perilous!  At some point there will be an update to the data, and people will ask "ok, who updated it?" and there will be no answer. Your access logs will show "anonymous" and that won't be a good feeling. 

So I think it's probably wise to use user-auth for both internal and external consumers. Just use a different OpenID connect provider! You can even create your own OpenID connect provider with Apigee, if you don't have an internal one already built. 

Then, in the proxy, you can

  • DecodeJWT - this shreds the fields of the inbound JWT into readable context variables.
  • examine the issuer - this is stored in a single context variable by the above step. 
  • invoke VerifyJWT1 if the JWT was issued by the "Internal" OIDC provider, invoke VerifyJWT2 if the JWT was issued by the external provider. The only difference in these policies is the URL of the JWKS.   The "if the JWT was issued by the internal provider" is something you configure with a Condition element, testing the extracted issuer value from the inbound JWT. 
    Important: if the issuer isn't recognized, you need to Raise a Fault!   So really there are three cases, known issuer #1, known issuer #2, or unknown issuer. 
  • In either case, call VerifyAPIKey on the clientid claim that is extracted from the inbound JWT. This will get you API Product-level authorization in Apigee. 

That would do it and keep the authentication model the same. 

You could also apply a similar technical approach as I just described, but instead of branching to one or the other VerifyJWT for verification, you could branch to either VerifyJWT for the OIDC-issued token, or OAuthV2/VerifyAccessToken for the Oauth2 token. 

BTW I take issue with your characterization of OAuth2 and OIDC as disparate things.  OIDC is just an authorization layer on top of OAuth2.  A token issued via an OIDC flow is really issued via an OAuth2 authorization_code grant_type.  OIDC is always OAuth2.  But OIDC specifies additional things, beyond what OAuth2 requires - things like the profile, issuing an ID token and an access token independently, the fact that tokens are JWT, and so on.  Anyway it's not quite right to say "one is OIDC and the other is OAuth2".  I think maybe what you mean is "one is a token issued via OIDC (which implies user authentication) and the other is issued via OAuth2 client_credentials grant_type (which implies no user authentication).  

But I do agree whole-heartedly, you do not want to copy/paste all your APIs and duplicate them, one for internal and one for external use. 

Ideally you can extract the authorization part (VerifyJWT or VerifyAccessToken etc) completely into its own sharedflow.  Codify it once, get it the way you want it, and then have each API embed a FlowCallout into that "Authentication" sharedflow in the request preflow.  Every API will get the same treatment. 

 

View solution in original post

2 REPLIES 2

I understand the words you're saying, but I have a different view.  I don't think it is a wise idea to expose APIs to sometimes require user authentication, and sometimes not. That seems perilous!  At some point there will be an update to the data, and people will ask "ok, who updated it?" and there will be no answer. Your access logs will show "anonymous" and that won't be a good feeling. 

So I think it's probably wise to use user-auth for both internal and external consumers. Just use a different OpenID connect provider! You can even create your own OpenID connect provider with Apigee, if you don't have an internal one already built. 

Then, in the proxy, you can

  • DecodeJWT - this shreds the fields of the inbound JWT into readable context variables.
  • examine the issuer - this is stored in a single context variable by the above step. 
  • invoke VerifyJWT1 if the JWT was issued by the "Internal" OIDC provider, invoke VerifyJWT2 if the JWT was issued by the external provider. The only difference in these policies is the URL of the JWKS.   The "if the JWT was issued by the internal provider" is something you configure with a Condition element, testing the extracted issuer value from the inbound JWT. 
    Important: if the issuer isn't recognized, you need to Raise a Fault!   So really there are three cases, known issuer #1, known issuer #2, or unknown issuer. 
  • In either case, call VerifyAPIKey on the clientid claim that is extracted from the inbound JWT. This will get you API Product-level authorization in Apigee. 

That would do it and keep the authentication model the same. 

You could also apply a similar technical approach as I just described, but instead of branching to one or the other VerifyJWT for verification, you could branch to either VerifyJWT for the OIDC-issued token, or OAuthV2/VerifyAccessToken for the Oauth2 token. 

BTW I take issue with your characterization of OAuth2 and OIDC as disparate things.  OIDC is just an authorization layer on top of OAuth2.  A token issued via an OIDC flow is really issued via an OAuth2 authorization_code grant_type.  OIDC is always OAuth2.  But OIDC specifies additional things, beyond what OAuth2 requires - things like the profile, issuing an ID token and an access token independently, the fact that tokens are JWT, and so on.  Anyway it's not quite right to say "one is OIDC and the other is OAuth2".  I think maybe what you mean is "one is a token issued via OIDC (which implies user authentication) and the other is issued via OAuth2 client_credentials grant_type (which implies no user authentication).  

But I do agree whole-heartedly, you do not want to copy/paste all your APIs and duplicate them, one for internal and one for external use. 

Ideally you can extract the authorization part (VerifyJWT or VerifyAccessToken etc) completely into its own sharedflow.  Codify it once, get it the way you want it, and then have each API embed a FlowCallout into that "Authentication" sharedflow in the request preflow.  Every API will get the same treatment. 

 

Thank you, that is a great and comprehensive answer - don't duplicate the API to cater for various authentication methods, or providers, deal with that logic in 1 API using things like the VerifyJWT and <condition> statements. Thanks again!