Skip to content

Instantly share code, notes, and snippets.

@timotheeguerin
Last active September 27, 2023 17:05
Show Gist options
  • Save timotheeguerin/56690786e61a436710dd647de9febc0f to your computer and use it in GitHub Desktop.
Save timotheeguerin/56690786e61a436710dd647de9febc0f to your computer and use it in GitHub Desktop.
Per operation authentication and scopes

Operation level authentication and scopes

Goals:

Proposal:

  • Allow @useAuth on all operations, interace, etc.
  • Using @useAuth at a lower level override the value at the higher level.
  • To specify scopes for a given operation specify different scope to the oauth2 scheme

Example 1: Override on operation

alias ServiceKeyAuth = ApiKeyAuth<ApiKeyLocation.header, "X-API-KEY">;
@useAuth(ServiceKeyAuth)
namespace MyService;

// Have to use X-API-KEY header auth.
op list(): FileInfo[];

// Could use
//  - X-API-KEY header
//  - token query
//  - no auth
@useAuth(ServiceKeyAuth | ApiKeyAuth<ApiKeyLocation.query, "token"> | NoAuth)
op download(fileId: string): bytes;

Example 2: Override on interface

alias ServiceKeyAuth = ApiKeyAuth<ApiKeyLocation.header, "X-API-KEY">;
@useAuth(ServiceKeyAuth)
namespace MyService;

// Have to use X-API-KEY header auth.
op list(): FileInfo[];


@useAuth(ServiceKeyAuth | ApiKeyAuth<ApiKeyLocation.query, "token"> | NoAuth)
interface FileManagement {
  // Could use
  //  - X-API-KEY header
  //  - token query
  //  - no auth
  op download(fileId: string): bytes;

  // Could use
  //  - X-API-KEY header
  //  - token query
  //  - no auth
  op upload(fileId: string, data: bytes): void;
}

Example 3: Specify scopes

alias ServiceFlow<T extends string[]> = {
  type: OAuth2FlowType.implicit;
  authorizationUrl: "https://api.example.com/oauth2/authorize";
  refreshUrl: "https://api.example.com/oauth2/refresh";
  scopes: T;
}
alias ServiceOAuth<T extends string[]> = OAuth2<[ServiceFlow<T>]>;

@useAuth(ServiceKeyAuth<["read", "write", "delete"]>)
namespace MyService;

// Have to use X-API-KEY header auth.
@useAuth(ServiceOAuth<["read"]>)
op list(): FileInfo[];

@useAuth(ServiceOAuth<["read"]>)
op read(): FileInfo;

@useAuth(ServiceOAuth<["write"]>)
op upload();

@useAuth(ServiceOAuth<["delete"]>)
op delete();

Pros:

  • works well with the current system
  • not breaking

Cons:

  • This is needs a decent amount of boilerplate.
  • Resolving the scope for each operation might not be that simple in the code

Proposal alternative 1:

Detach the scopes from the flow

alias ServiceFlow = {
  type: OAuth2FlowType.implicit;
  authorizationUrl: "https://api.example.com/oauth2/authorize";
  refreshUrl: "https://api.example.com/oauth2/refresh";
};

alias ServiceOAuth<T extends string[]> = OAuth2<[ServiceFlow], T>;

This simplify the reusable template you might have to create specially if you had multiple flows

alias ServiceFlow1<T extends string[]> = {
  type: OAuth2FlowType.implicit;
  authorizationUrl: "https://api.example.com/oauth2/authorize";
  refreshUrl: "https://api.example.com/oauth2/refresh";
  scopes: T;
};

alias ServiceFlow2<T extends string[]> = {
  type: OAuth2FlowType.implicit;
  authorizationUrl: "https://api.example.com/oauth2/authorize";
  refreshUrl: "https://api.example.com/oauth2/refresh";
  scopes: T;
};

alias ServiceOAuth<T extends string[]> = OAuth2<[ServiceFlow1, ServiceFlow2], T>;
alias ServiceFlow1 = {
  type: OAuth2FlowType.implicit;
  authorizationUrl: "https://api.example.com/oauth2/authorize";
  refreshUrl: "https://api.example.com/oauth2/refresh";
};

alias ServiceFlow2 = {
  type: OAuth2FlowType.implicit;
  authorizationUrl: "https://api.example.com/oauth2/authorize";
  refreshUrl: "https://api.example.com/oauth2/refresh";
};

alias ServiceOAuth<T extends string[]> = OAuth2<[ServiceFlow1, ServiceFlow2], T>;

Pros:

  • Less verbose

Cons:

  • Could be breaking depending on how we remove the scopes property from the Flow (might be able to keep as backward compatible)
  • You now cannot have different scopes for different flows. I don't know of a use case for this but openapi3 was designed that way.
  • Still not the cleansest at the operation level.

Proposal alternative 2:

To build on the previous proposal we could add a new decorator @authScope that would be able to specify the scopes separately from the auth. The decorator would define the scopes for the auth scheme that would use a scope.

@useAuth(OAuth2<{
  type: OAuth2FlowType.implicit;
  authorizationUrl: "https://api.example.com/oauth2/authorize";
  refreshUrl: "https://api.example.com/oauth2/refresh";
}>)
@authScopes("read", "write", "delete")
namespace MyService;

// Have to use X-API-KEY header auth.
@authScopes("read")
op list(): FileInfo[];

@authScopes("read")
op read(): FileInfo;

@authScopes("write")
op upload();

@authScopes("delete")
op delete();

Pros:

  • Much cleaner

Cons:

  • Still could be breaking same as previous alternative
  • Scopes cannot be different for different auth scheme. Not sure if that's a real use case but it would be blocking in case it is.
@nicneate
Copy link

Thanks for progressing this. Happy to leave the design to you, but FWIW I don't have a requirement to support different scopes in different auth schemes (which sounds like a nightmare).

@timotheeguerin
Copy link
Author

Update to proposal:

(using alternative 2)

Seperate the scope from the security definition. And add @authScope decorator where the first argument is the security definition.

alias MyOAuth2 = OAuth2<{
  type: OAuth2FlowType.implicit;
  authorizationUrl: "https://api.example.com/oauth2/authorize";
  refreshUrl: "https://api.example.com/oauth2/refresh";
}>;
@useAuth(MyOAuth2)
@authScopes(MyOAuth2, "read", "write", "delete")
namespace MyService;

// Have to use X-API-KEY header auth.
@authScopes(MyOAuth2, "read")
op list(): FileInfo[];

@authScopes(MyOAuth2, "read")
op read(): FileInfo;

@authScopes(MyOAuth2, "write")
op upload();

@authScopes(MyOAuth2, "delete")
op delete();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment