gam-oauth-authenticator
v1.1.5
Published
Headless OAuth 2.0 client for GeneXus GAM Identity Provider
Maintainers
Readme
GAM OAuth Authenticator
Um módulo Node.js headless e independente de framework para integração OAuth 2.0 com GeneXus GAM (GeneXus Access Manager) como Identity Provider.
Características
- ✅ Headless - sem dependências de frameworks HTTP (Express, Next.js, Fastify, etc.)
- ✅ TypeScript nativo com tipos completos
- ✅ Compatível com Node.js 20+ (usa
fetchnativo) - ✅ Suporte a PKCE (Proof Key for Code Exchange)
- ✅ Totalmente testável e configurável
- ✅ Tratamento de erros explícito
- ✅ Single Logout (SLO) – URL de logout único com o GAM
Instalação
npm install gam-oauth-authenticatorRequisitos
- Node.js 20.0.0 ou superior
Uso Básico
Criar o Client
import { createGamOAuthClient } from "gam-oauth-authenticator";
const gamClient = createGamOAuthClient({
baseUrl: "https://idp.exemplo.com/VirtualDir",
clientId: "my-client-id",
clientSecret: "my-client-secret",
defaultScopes: ["gam_user_data"],
});1. Obter access token (por credenciais)
Obtenha um Bearer token no endpoint oauth/gam/v2.0/access_token sem redirecionamento OAuth. O mesmo accessToken é usado em getUserInfo, refreshToken e logoutWithAccessToken.
Fluxos suportados: usuário/senha (GAMLocal), OTP (One Time Password), API Key e 2FA (segundo fator) via additionalParameters.
Usuário e senha (GAMLocal)
try {
const tokens = await gamClient.getAccessTokenWithCredentials({
username: "usuario",
password: "senha",
scope: "gam_user_data", // opcional
grantType: "GAMLocal", // opcional, padrão GAMLocal
// repository: "MeuRepo", // opcional, para GAM multi-repositório
});
console.log(tokens.accessToken);
} catch (error) {
if (error instanceof GamNeedsSecondFactorError) {
// 2FA: chame novamente com password = código TOTP e additionalParameters (OTPStep "2", etc.)
} else if (error instanceof GamNeedsOtpError) {
// OTP por e-mail ou 401 OTP step 1: chame com otpStep: 2 e password = código
}
}OTP (One Time Password)
Dois passos: (1) solicitar envio do código; (2) enviar o código para obter o token. O GAM envia o código por e-mail ou SMS (configurável). Documentação: GAM - One Time Password (OTP).
// Passo 1: solicitar código (GAM envia por e-mail/SMS). Resposta 401 com Code 400 → GamNeedsOtpError
try {
await gamClient.getAccessTokenWithCredentials({
username: "usuario",
authenticationTypeName: "gam-otp-web", // nome do tipo OTP no GAM
otpStep: 1,
});
} catch (e) {
if (e instanceof GamNeedsOtpError) {
// Mostrar campo para o usuário informar o código recebido
}
}
// Passo 2: enviar código recebido
const tokens = await gamClient.getAccessTokenWithCredentials({
username: "usuario",
password: "123456", // código OTP recebido
authenticationTypeName: "gam-otp-web",
otpStep: 2,
});API Key
Autenticação com chave gerada no GAM para o usuário/aplicação. No fluxo API Key o username não é obrigatório. Documentação: HowTo: Use API Key to request services from an application.
const tokens = await gamClient.getAccessTokenWithCredentials({
apiKey: "chave-api-gerada-no-gam",
authenticationTypeName: "apikey", // opcional, padrão "apikey"
scope: "gam_user_data",
// username opcional no fluxo API Key
});2FA (segundo fator)
Se o GAM exigir 2FA ou OTP por e-mail no fluxo GAMLocal, a primeira chamada retorna 202 com GamNeedsSecondFactorError ou GamNeedsOtpError. Chame novamente com password: codigo e, no 2FA, additionalParameters com OTPStep: "2" e UseTwoFactorAuthentication: "true".
2. Construir URL de Login (fluxo OAuth)
const signinUrl = gamClient.buildSigninUrl({
redirectUri: "https://myapp.com/callback",
state: "random-state-string",
scopes: ["gam_user_data"], // opcional, usa defaultScopes se não fornecido
});
// Redirecionar o usuário para signinUrlQuando o GAM tem múltiplos tipos de autenticação (ex.: Gov.br, Google, local), use authenticationTypeName para direcionar o usuário ao IdP desejado:
// Login com Gov.br
const signinUrlGovBr = gamClient.buildSigninUrl({
redirectUri: "https://myapp.com/callback",
state: "random-state-string",
authenticationTypeName: "pmlink1_govbr", // nome configurado no GAM
});
// Login com Google
const signinUrlGoogle = gamClient.buildSigninUrl({
redirectUri: "https://myapp.com/callback",
state: "random-state-string",
authenticationTypeName: "pmlink1_google",
});3. Trocar Código por Token (fluxo OAuth)
try {
const tokens = await gamClient.exchangeCodeForToken({
code: "authorization-code-from-callback",
redirectUri: "https://myapp.com/callback", // deve corresponder exatamente
});
console.log(tokens.accessToken);
console.log(tokens.refreshToken); // pode ser undefined
console.log(tokens.expiresIn); // em segundos
} catch (error) {
if (error instanceof GamTokenError) {
console.error("Erro OAuth:", error.error, error.errorDescription);
}
}4. Obter Informações do Usuário
try {
const userInfo = await gamClient.getUserInfo(tokens.accessToken);
console.log(userInfo.guid); // ID do usuário (GAM GUID)
console.log(userInfo.name);
console.log(userInfo.email);
console.log(userInfo.roles);
} catch (error) {
if (error instanceof GamUserInfoError) {
console.error("Erro ao buscar informações do usuário:", error.message);
}
}5. Renovar Token (Opcional)
try {
const newTokens = await gamClient.refreshToken(tokens.refreshToken);
console.log(newTokens.accessToken);
console.log(newTokens.refreshToken); // novo refresh token (pode ser undefined)
} catch (error) {
if (error instanceof GamTokenError) {
console.error("Erro ao renovar token:", error.message);
}
}6. Single Logout (Logout único)
Para encerrar a sessão no GAM e redirecionar o usuário de volta à sua aplicação, use a URL de signout. O GAM invalida a sessão do usuário no IdP e redireciona para o redirectUri informado. Recomenda-se validar o parâmetro state no callback (igual ao fluxo de login) para proteção CSRF.
import crypto from "crypto";
// 1. Gerar e guardar o state (ex.: em cookie ou sessão)
const state = crypto.randomBytes(32).toString("hex");
// 2. Construir a URL de logout e redirecionar o usuário
const signoutUrl = gamClient.buildSignoutUrl({
redirectUri: "https://myapp.com/auth/logout/callback",
token: tokens.accessToken, // access token da sessão atual
state,
});
// Redirecionar o usuário para signoutUrl
// window.location.href = signoutUrl; (no browser)
// res.redirect(signoutUrl); (no Express, etc.)No callback (/auth/logout/callback), valide que o state recebido na query corresponde ao que foi guardado; em seguida, limpe a sessão/local storage da sua aplicação e redirecione para a página inicial ou login.
7. Logout para token mobile (access token)
Para tokens obtidos via login por credenciais (getAccessTokenWithCredentials), use o endpoint de logout mobile para invalidar o access token no GAM sem redirecionamento. O serviço REST recebe o token no header Authorization e invalida a sessão no servidor.
Endpoint utilizado: POST /oauth/logout (baseUrl + path).
try {
const result = await gamClient.logoutWithAccessToken(tokens.accessToken);
if (result.code === "200") {
// Logout concluído no GAM; limpe a sessão local (cookie, storage, etc.)
// e redirecione ou responda ao cliente
}
} catch (error) {
if (error instanceof GamTokenError) {
console.error("Erro ao invalidar token:", error.message);
} else if (error instanceof GamNetworkError) {
console.error("Erro de rede no logout:", error.message);
}
}Em aplicações BFF: ao receber uma requisição de logout do cliente, chame logoutWithAccessToken(accessToken) com o token da sessão atual e, em seguida, remova o cookie ou a sessão no seu backend.
Documentação oficial: HowTo: Sign out from mobile applications using GAM.
8. Validar permissões (External Authorization API)
É possível validar permissões do usuário através de uma API externa (por exemplo, uma API GeneXus que consome GAM). O client oferece dois métodos que chamam o endpoint POST /User/CheckPermissions dessa API, autenticando com o Bearer token do usuário.
Configuração: a URL base da API de permissões é opcional. Se não for informada em externalAuthorizationApiUrl, o módulo usa o mesmo baseUrl do GAM e assume o path /ExternalAuthorization (ou seja, baseUrl + "/ExternalAuthorization"). Assim, basta configurar baseUrl (por exemplo via GAM_BASE_URL) para que a validação de permissões use essa mesma base.
const gamClient = createGamOAuthClient({
baseUrl: "https://idp.exemplo.com/VirtualDir",
clientId: "my-client-id",
clientSecret: "my-client-secret",
// externalAuthorizationApiUrl opcional: padrão = baseUrl + "/ExternalAuthorization"
});O pacote inclui o arquivo ExternalAuthorization.xpz, um objeto GeneXus de exemplo que expõe a API de validação de permissões (POST /User/CheckPermissions). Em aplicações GeneXus, você pode importar esse xpz para disponibilizar o endpoint esperado por checkPermission e checkPermissions (por exemplo em baseUrl + "/ExternalAuthorization").
Uma permissão — retorna true ou false:
try {
const authorized = await gamClient.checkPermission(
accessToken,
"NomeDaPermissao",
{ applicationGUID: "guid-da-aplicacao" }
);
if (authorized) {
// usuário tem a permissão
}
} catch (error) {
if (error instanceof GamPermissionCheckError) {
console.error("Falha na API de permissões:", error.statusCode, error.responseBody);
}
if (error instanceof GamConfigurationError) {
console.error("Erro de configuração:", error.message);
}
}Várias permissões — retorna a lista com o resultado de cada uma:
try {
const result = await gamClient.checkPermissions(
accessToken,
["Permissao1", "Permissao2"],
{ applicationGUID: "guid-da-aplicacao" }
);
for (const p of result.permissions) {
console.log(p.permissionName, p.isAuthorized);
}
if (result.messages && result.messages.length > 0) {
// mensagens retornadas pela API (opcional)
}
} catch (error) {
if (error instanceof GamPermissionCheckError) {
console.error("Falha na API de permissões:", error.message);
}
}A API esperada recebe no body ApplicationGUID (opcional) e PermissionNames (array de strings) e retorna PermissionAuthorization.Permissions com PermissionName e IsAuthorized para cada item.
Exemplo de aplicação
Uma aplicação de exemplo que demonstra o uso do módulo com Express está disponível no repositório, na pasta examples/. A documentação de instalação, configuração e execução está em examples/README.md. O pacote publicado no npm não inclui essa pasta.
Uso com PKCE
import { createGamOAuthClient } from "gam-oauth-authenticator";
import crypto from "crypto";
// Gerar code verifier e challenge (você precisa implementar isso)
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto
.createHash("sha256")
.update(codeVerifier)
.digest("base64url");
// Construir URL com PKCE
const signinUrl = gamClient.buildSigninUrl({
redirectUri: "https://myapp.com/callback",
state: "random-state-string",
pkce: {
codeChallenge,
method: "S256",
},
});
// Ao trocar o código, incluir o code verifier
const tokens = await gamClient.exchangeCodeForToken({
code: "authorization-code",
redirectUri: "https://myapp.com/callback",
codeVerifier, // necessário quando PKCE foi usado
});Tratamento de Erros
O módulo exporta classes de erro específicas:
import {
GamOAuthError,
GamTokenError,
GamUserInfoError,
GamNetworkError,
GamConfigurationError,
GamPermissionCheckError,
GamNeedsSecondFactorError,
GamNeedsOtpError,
} from "gam-oauth-authenticator";
try {
// ... operação OAuth
} catch (error) {
if (error instanceof GamTokenError) {
// Erro no token endpoint
console.error(error.error); // código do erro OAuth
console.error(error.errorDescription); // descrição do erro
console.error(error.statusCode); // código HTTP
} else if (error instanceof GamUserInfoError) {
// Erro no userinfo endpoint
console.error(error.statusCode);
console.error(error.responseBody);
} else if (error instanceof GamNetworkError) {
// Erro de rede
console.error(error.originalError);
} else if (error instanceof GamConfigurationError) {
// Erro de configuração (ex.: externalAuthorizationApiUrl ausente)
console.error(error.message);
} else if (error instanceof GamPermissionCheckError) {
// Erro na API de permissões (External Authorization)
console.error(error.statusCode, error.responseBody);
} else if (error instanceof GamNeedsSecondFactorError) {
// 2FA: chamar novamente com password = código TOTP e additionalParameters
console.error(error.message);
} else if (error instanceof GamNeedsOtpError) {
// OTP: passo 1 (código enviado) ou 202; chamar com otpStep: 2 e password = código
console.error(error.message);
}
}API Reference
createGamOAuthClient(config)
Cria uma instância do client OAuth GAM.
Parâmetros:
config.baseUrl(string, obrigatório): URL base do Identity Provider GAMconfig.clientId(string, obrigatório): ID do cliente OAuthconfig.clientSecret(string, obrigatório): Secret do cliente OAuthconfig.defaultScopes(string[], opcional): Escopos padrão (padrão:["gam_user_data"])config.externalAuthorizationApiUrl(string, opcional): URL base da API External Authorization; se omitido, usabaseUrl + "/ExternalAuthorization"config.fetch(Function, opcional): Implementação customizada de fetch (útil para testes)
Retorna: GamOAuthClient
GamOAuthClient.buildSigninUrl(options)
Constrói a URL de autorização para login.
Parâmetros:
options.redirectUri(string, obrigatório): URI de redirecionamentooptions.state(string, obrigatório): Parâmetro state para proteção CSRFoptions.scopes(string[], opcional): Escopos a solicitaroptions.authenticationTypeName(string, opcional): Nome do tipo de autenticação no GAM (ex.: Gov.br, Google); obrigatório quando há múltiplos tipos configuradosoptions.pkce(object, opcional): Configuração PKCEpkce.codeChallenge(string): Code challengepkce.method("S256"): Método do challenge
Retorna: string - URL completa de autorização
GamOAuthClient.buildSignoutUrl(options)
Constrói a URL de signout (single logout) no GAM. O usuário deve ser redirecionado para essa URL; o GAM encerra a sessão e redireciona para redirectUri. O state deve ser guardado (ex.: cookie) e validado no callback.
Parâmetros:
options.redirectUri(string, obrigatório): URI para onde o usuário será redirecionado após o logout no GAMoptions.token(string, obrigatório): Access token da sessão atual (enviado ao GAM para invalidar a sessão)options.state(string, obrigatório): Valor de state para proteção CSRF (guardar e validar no callback)
Retorna: string - URL completa de signout
GamOAuthClient.exchangeCodeForToken(options)
Troca um código de autorização por tokens.
Parâmetros:
options.code(string, obrigatório): Código de autorizaçãooptions.redirectUri(string, obrigatório): URI de redirecionamento usado na autorizaçãooptions.codeVerifier(string, opcional): Code verifier para PKCE
Retorna: Promise<TokenResponse>
GamOAuthClient.getAccessTokenWithCredentials(options)
Obtém um access token (Bearer) no endpoint POST oauth/gam/v2.0/access_token sem redirecionamento. Suporta: usuário/senha (GAMLocal), OTP, API Key e 2FA.
Parâmetros:
options.username(string, opcional): Nome de usuário (obrigatório em OTP e fluxo por senha; opcional em API Key)options.password(string, opcional): Senha (obrigatória em GAMLocal e OTP step 2; omitida em API Key e OTP step 1)options.scope(string, opcional): Escopo (padrão: defaultScopes do client)options.grantType(string, opcional): Grant type (padrão:"GAMLocal"; OTP/API Key usam"password"internamente)options.repository(string, opcional): Repositório GAM (emadditional_parameters.Repository)options.authenticationTypeName(string, opcional): Nome do tipo de autenticação no GAM; obrigatório para OTP; para API Key o padrão é"apikey"options.otpStep(1 | 2, opcional): OTP: 1 = solicitar código; 2 = enviar código (password = código)options.apiKey(string, opcional): API Key gerada no GAM; quando informada, não se envia passwordoptions.additionalParameters(object, opcional): Parâmetros extras (ex. 2FA:OTPStep: "2",UseTwoFactorAuthentication: "true")
Retorna: Promise<TokenResponse> – accessToken, refreshToken, expiresIn, tokenType.
Erros: 401 com Code 400 (OTP step 1) ou 202 com código 400 → GamNeedsOtpError; 202 com código 410 → GamNeedsSecondFactorError. Chame novamente com o código (password) e, no 2FA, additionalParameters adequados.
GamOAuthClient.getUserInfo(accessToken)
Obtém informações do usuário autenticado.
Parâmetros:
accessToken(string, obrigatório): Access token
Retorna: Promise<UserInfoResponse>
GamOAuthClient.refreshToken(refreshToken, options?)
Renova um access token usando um refresh token.
Parâmetros:
refreshToken(string, obrigatório): Refresh tokenoptions.useMobileEndpoint(boolean, opcional): Setrue, usa o endpoint/oauth/access_token(para tokens obtidos viagetAccessTokenWithCredentials)
Retorna: Promise<RefreshTokenResponse>
GamOAuthClient.logoutWithAccessToken(accessToken)
Invalida o access token no servidor GAM (fluxo mobile; não redireciona o usuário). Use para tokens obtidos via getAccessTokenWithCredentials.
Parâmetros:
accessToken(string, obrigatório): Access token obtido no login mobile (credenciais)
Retorna: Promise<LogoutResponse> – objeto com code (string); sucesso quando code === "200".
GamOAuthClient.checkPermission(accessToken, permissionName, options)
Valida uma permissão do usuário via API External Authorization (GeneXus). Usa baseUrl + "/ExternalAuthorization" quando externalAuthorizationApiUrl não é informada.
Parâmetros:
accessToken(string, obrigatório): Access token do usuáriopermissionName(string, obrigatório): Nome da permissãooptions(obrigatório):{ applicationGUID: string }— GUID da aplicação (conforme contrato da API)
Retorna: Promise<boolean> – true se autorizado, false caso contrário.
Erros: GamPermissionCheckError em falha da API (ex.: 404, resposta inválida).
GamOAuthClient.checkPermissions(accessToken, permissionNames, options)
Valida várias permissões do usuário em uma única chamada à API External Authorization.
Parâmetros:
accessToken(string, obrigatório): Access token do usuáriopermissionNames(string[], obrigatório): Array com os nomes das permissõesoptions(obrigatório):{ applicationGUID: string }— GUID da aplicação
Retorna: Promise<CheckPermissionsResult> – { permissions: Array<{ permissionName, isAuthorized }>, messages?: GeneXusMessage[] }.
Erros: Os mesmos de checkPermission.
Endpoints GAM
O módulo usa os seguintes endpoints do GAM:
- Authorization:
/oauth/gam/signin - Token (Web):
/oauth/gam/access_token - Token (credenciais/OTP/API Key):
/oauth/gam/v2.0/access_token - Logout mobile:
POST /oauth/logout– invalida o access token no GAM (uso com token obtido via credenciais) - UserInfo:
/oauth/gam/userinfo - Signout (Single Logout):
/oauth/gam/signout
Documentação oficial:
