Implementação do Open Authorization Framework do InterSystems IRIS (OAuth 2.0) - parte 2
Authenticating and Authorizing against Cache´ OAuth2 provider
This page demo shows how to call Cache´ API functions using OAuth2 authorization.
We are going to call Cache´ authentication and authorization server to grant our application access to data stored at another
Cache´ server.
>
// Get the url for authorization endpoint with appropriate redirect and scopes.
// The returned url is used in the button below.
// DK: use 'dankut' account to authenticate!
set scope="openid profile scope1 scope2"
set url=##class(%SYS.OAuth2.Authorization).GetAuthorizationCodeEndpoint(
..#OAUTH2APPNAME,
scope,
..#OAUTH2CLIENTREDIRECTURI,
.properties,
.isAuthorized,
.sc)
if $$$ISERR(sc) {
write "GetAuthorizationCodeEndpoint Error="
write ..EscapeHTML($system.Status.GetErrorText(sc))_"
",!
}
&html<
Página 2
Class Web.OAUTH2.Cache2N Extends %CSP.Page
{
Parameter OAUTH2APPNAME = "demo client";
Parameter OAUTH2ROOT = "https://dk-gs2016/resserver";
Parameter SSLCONFIG = "SSL4CLIENT";
ClassMethod OnPage() As %Status
{
&html<
>
// Check if we have an access token from oauth2 server
set isAuthorized=##class(%SYS.OAuth2.AccessToken).IsAuthorized(..#OAUTH2APPNAME,,"scope1 scope2",.accessToken,.idtoken,.responseProperties,.error)
// Continue with further checks if an access token exists.
// Below are all possible tests and may not be needed in all cases.
// The JSON object which is returned for each test is just displayed.
if isAuthorized {
write "Authorized!
",!
// Validate and get the details from the access token, if it is a JWT.
set valid=##class(%SYS.OAuth2.Validation).ValidateJWT(..#OAUTH2APPNAME,accessToken,"scope1 scope2",,.jsonObject,.securityParameters,.sc)
if $$$ISOK(sc) {
if valid {
write "Valid JWT"_"
",!
} else {
write "Invalid JWT"_"
",!
}
write "Access token="
do jsonObject.%ToJSON()
write "
",!
} else {
write "JWT Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"
",!
}
write "
",!
// Call the introspection endpoint and display result -- see RFC 7662.
set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection(..#OAUTH2APPNAME,accessToken,.jsonObject)
if $$$ISOK(sc) {
write "Introspection="
do jsonObject.%ToJSON()
write "
",!
} else {
write "Introspection Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"
",!
}
write "
",!
if idtoken'="" {
// Validate and display the IDToken -- see OpenID Connect Core specification.
set valid=##class(%SYS.OAuth2.Validation).ValidateIDToken(
..#OAUTH2APPNAME,
idtoken,
accessToken,,,
.jsonObject,
.securityParameters,
.sc)
if $$$ISOK(sc) {
if valid {
write "Valid IDToken"_"
",!
} else {
write "Invalid IDToken"_"
",!
}
write "IDToken="
do jsonObject.%ToJSON()
write "
",!
} else {
write "IDToken Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"
",!
}
} else {
write "No IDToken returned"_"
",!
}
write "
",!
// not needed for the application logic, but provides information about user that we can pass to Delegated authentication
// Call the userinfo endpoint and display the result -- see OpenID Connect Core specification.
set sc=##class(%SYS.OAuth2.AccessToken).GetUserinfo(
..#OAUTH2APPNAME,
accessToken,,
.jsonObject)
if $$$ISOK(sc) {
write "Userinfo="
do jsonObject.%ToJSON()
write "
",!
} else {
write "Userinfo Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"
",!
}
write "",!
/***************************************************
* *
* Call the resource server and display result. *
* *
***************************************************/
// option 1 - resource server - by definition - trusts data coming from authorization server,
// so it serves data to whoever is asking
// as long as access token passed to resource server is valid
// option 2 - alternatively, you can use delegated authentication (OpenID Connect)
// and call into another CSP application (with delegated authentication protection)
// - that's what we do here in this demo
write "<4>Call resource server (delegated auth)","",!
set httpRequest=##class(%Net.HttpRequest).%New()
// AddAccessToken adds the current access token to the request.
set sc=##class(%SYS.OAuth2.AccessToken).AddAccessToken(
httpRequest,,
..#SSLCONFIG,
..#OAUTH2APPNAME)
if $$$ISOK(sc) {
set sc=httpRequest.Get(..#OAUTH2ROOT_"/csp/portfolio/oauth2test.demoResource.cls")
}
if $$$ISOK(sc) {
set body=httpRequest.HttpResponse.Data
if $isobject(body) {
do body.Rewind()
set body=body.Read()
}
write body,"
",!
}
if $$$ISERR(sc) {
write "Resource Server Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"
",!
}
write "
",!
write "
Call resource server - no auth, just token validity check","
",!
set httpRequest=##class(%Net.HttpRequest).%New()
// AddAccessToken adds the current access token to the request.
set sc=##class(%SYS.OAuth2.AccessToken).AddAccessToken(
httpRequest,,
..#SSLCONFIG,
..#OAUTH2APPNAME)
if $$$ISOK(sc) {
set sc=httpRequest.Get(..#OAUTH2ROOT_"/csp/portfolio2/oauth2test.demoResource.cls")
}
if $$$ISOK(sc) {
set body=httpRequest.HttpResponse.Data
if $isobject(body) {
do body.Rewind()
set body=body.Read()
}
write body,"
",!
}
if $$$ISERR(sc) {
write "Resource Server Error="_..EscapeHTML($system.Status.GetErrorText(sc))_"
",!
}
write "
",!
} else {
write "Not Authorized!",!
write "Authorize me"
}
&html<>
Quit $$$OK
}
}
As seguintes capturas de tela retratam o processamento:
Página de login do servidor de autenticação do OpenID Connect/autorização na instância AUTHSERVER

Página de consentimento do usuário em AUTHSERVER

E, por fim, a página resultante

Como você pode ver, lendo o código, realmente quase não há diferença em relação ao código do cliente que mostramos na parte 1. Há algo novo na página 2. São algumas informações de depuração e a verificação da validade do JWT. Depois de validar o JWT retornado, podemos realizar a introspeção dos dados do AUTHSERVER sobre a identidade do usuário. Simplesmente apresentamos essas informações nos resultados da página, mas podemos fazer mais com elas. Como no caso de uso de um médico externo mencionado acima, podemos usar as informações de identidade e transmiti-las ao servidor de recursos para fins de autenticação, se necessário. Ou apenas passar essa informação como um parâmetro para a chamada da API ao servidor de recursos.
Os próximos parágrafos descrevem como usamos as informações de identidade do usuário em mais detalhes.
Aplicativo de recurso
O servidor de recursos pode ser o mesmo servidor que o servidor de autorização/autenticação e, muitas vezes, esse é o caso. No entanto, na nossa demonstração, criamos para os dois servidores instâncias do InterSystems IRIS separadas.
Então, temos dois casos possíveis, como trabalhar com o contexto de segurança no servidor de recursos.
Alternativa 1 — sem autenticação
Esse é o caso simples. O servidor de autorização/autenticação são apenas a mesma instância do Caché. Nesse caso, podemos simplesmente transmitir o token de acesso a um aplicativo CSP, que é criado especialmente para um único propósito — enviar dados a aplicativos clientes que usam o OAUTH para autorizar a solicitação de dados.
A configuração do aplicativo CSP de recurso (chamamos de /csp/portfolio2) pode parecer com a captura de tela abaixo.

Colocamos o mínimo de segurança na definição do aplicativo, permitindo que apenas a página CSP específica seja executada.
Como opção, o servidor de recursos pode fornecer uma API REST em vez de páginas da Web clássicas. Em situações reais, o refinamento do contexto de segurança depende do usuário.
Um exemplo de código-fonte:
Class oauth2test.demoResource Extends %CSP.Page
{
ClassMethod OnPage() As %Status
{
set accessToken=##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.sc)
if $$$ISOK(sc) {
set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection("RESSERVER resource",accessToken,.jsonObject)
if $$$ISOK(sc) {
// optionally validate against fields in jsonObject
w "Hello from Caché server: /csp/portfolio2 application!
"
w "running code as $username = "_$username_" with following $roles = "_$roles_" at node "_$p($zu(86),"*",2)_"."
}
} else {
w "
NOT AUTHORIZED!
"
w ""
w
i $d(%objlasterror) d $system.OBJ.DisplayError()
w ""
}
Quit $$$OK
}
}
Alternativa 2 — autenticação delegada
Esse é outro caso extrema, queremos usar a identidade do usuário no servidor de recursos o máximo possível, como se o usuário estivesse trabalhando com o mesmo contexto de segurança que os usuários internos do servidor de recursos.
Uma das opções possíveis é usar a autenticação delegada.
Para essa definição funcionar, precisamos concluir mais algumas etapas para configurar o servidor de recursos.
· Ativar a autenticação delegada
· Fornecer a rotina ZAUTHENTICATE
· Configurar o Web application (no nosso caso, chamamos em /csp/portfolio)
A implementação da rotina ZAUTHENTICATE é bastante simples e direta, já que confiamos no AUTHSERVER que forneceu a identidade do usuário e o escopo (perfil de segurança) dele, então basta aceitar o nome de usuário e transmitir com o escopo ao banco de dados de usuários do servidor de recursos (com a tradução necessária entre o escopo do OAUTH e as funções do InterSystems IRIS). É isso. O resto é realizado perfeitamente pelo InterSystems IRIS.
Veja o exemplo de uma rotina ZAUTHENTICATE
#include %occErrors
#include %occInclude
ZAUTHENTICATE(ServiceName, Namespace, Username, Password, Credentials, Properties) PUBLIC
{
set tRes=$SYSTEM.Status.OK()
try {
set Properties("FullName")="OAuth account "_Username
//set Properties("Roles")=Credentials("scope")
set Properties("Username")=Username
//set Properties("Password")=Password
// temporary hack as currently we can't pass Credentials array from GetCredentials() method
set Properties("Password")="xxx" // we don't really care about oauth2 account password
set Properties("Roles")=Password
} catch (ex) {
set tRes=$SYSTEM.Status.Error($$$AccessDenied)
}
quit tRes
}
GetCredentials(ServiceName,Namespace,Username,Password,Credentials) Public
{
s ts=$zts
set tRes=$SYSTEM.Status.Error($$$AccessDenied)
try {
If ServiceName="%Service_CSP" {
set accessToken=##class(%SYS.OAuth2.AccessToken).GetAccessTokenFromRequest(.sc)
if $$$ISOK(sc) {
set sc=##class(%SYS.OAuth2.AccessToken).GetIntrospection("RESSERVER resource",accessToken,.jsonObject)
if $$$ISOK(sc) {
// todo: watch out for potential collision between standard account and delegated (openid) one!
set Username=jsonObject.username
set Credentials("scope")=$p(jsonObject.scope,"openid profile ",2)
set Credentials("namespace")=Namespace
// temporary hack
//set Password="xxx"
set Password=$tr(Credentials("scope")," ",",")
set tRes=$SYSTEM.Status.OK()
} else {
set tRes=$SYSTEM.Status.Error($$$GetCredentialsFailed)
}
}
} else {
set tRes=$SYSTEM.Status.Error($$$AccessDenied)
}
} catch (ex) {
set tRes=$SYSTEM.Status.Error($$$GetCredentialsFailed)
}
Quit tRes
}
A própria página CSP pode ser bastante simples:
Class oauth2test.demoResource Extends %CSP.Page
{
ClassMethod OnPage() As %Status
{
// access token authentication is performed by means of Delegated authentication!
// no need to do it, again, here
// This is a dummy resource server which just gets the access token from the request and
// uses the introspection endpoint to ensure that the access token is valid.
// Normally the response would not be security related, but would contain some interesting
// data based on the request parameters.
w "Hello from Caché server: /csp/portfolio application!
"
w "running code as $username = "_$username_" with following $roles = "_$roles_" at node "_$p($zu(86),"*",2)_"."
Quit $$$OK
}
}
Por fim, a configuração do Web application para /csp/portfolio

Se você estiver muito paranoico, pode definir Classes permitidas como fizemos na primeira variante. Ou, novamente, use a API REST. No entanto, isso está muito além do escopo do nosso tema.
Na próxima vez, vamos explicar classes individuais, apresentadas pelo framework OAUTH do InterSystems IRIS. Descreveremos as APIs e quando/onde chamá-las.
[1] Quando mencionamos o OAUTH, queremos dizer o OAuth 2.0 conforme especificado no RFC 6749 -
[2] O OpenID Connect é mantido pela OpenID Foundation –