funifier-mcp 0.2.25 → 0.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/funifier.mdc +38 -41
- package/.github/copilot-instructions.md +38 -41
- package/AGENTS.md +56 -49
- package/README.md +40 -22
- package/datasource-funifier-docs/.coverage.json +326 -0
- package/datasource-funifier-docs/.validation.json +593 -0
- package/datasource-funifier-docs/knowledge/guides/aggregates.md +182 -70
- package/datasource-funifier-docs/knowledge/guides/database-access.md +174 -88
- package/datasource-funifier-docs/knowledge/guides/java-entities.md +294 -204
- package/datasource-funifier-docs/knowledge/guides/java-libraries.md +202 -226
- package/datasource-funifier-docs/knowledge/guides/java-managers.md +343 -265
- package/datasource-funifier-docs/knowledge/guides/trigger-examples.md +180 -236
- package/datasource-funifier-docs/knowledge/guides/triggers-guide.md +273 -191
- package/datasource-funifier-docs/knowledge/index.md +5 -2
- package/datasource-funifier-docs/knowledge/modules/achievement.md +1126 -28
- package/datasource-funifier-docs/knowledge/modules/action-log.md +469 -62
- package/datasource-funifier-docs/knowledge/modules/action.md +522 -70
- package/datasource-funifier-docs/knowledge/modules/auth.md +718 -69
- package/datasource-funifier-docs/knowledge/modules/avatar.md +483 -18
- package/datasource-funifier-docs/knowledge/modules/backup.md +603 -25
- package/datasource-funifier-docs/knowledge/modules/challenge.md +1048 -220
- package/datasource-funifier-docs/knowledge/modules/compact.md +469 -26
- package/datasource-funifier-docs/knowledge/modules/competition.md +811 -109
- package/datasource-funifier-docs/knowledge/modules/crossword.md +504 -28
- package/datasource-funifier-docs/knowledge/modules/csv-data.md +645 -20
- package/datasource-funifier-docs/knowledge/modules/custom-object.md +701 -36
- package/datasource-funifier-docs/knowledge/modules/database.md +730 -164
- package/datasource-funifier-docs/knowledge/modules/folder.md +1011 -77
- package/datasource-funifier-docs/knowledge/modules/kpi-formulas.md +410 -15
- package/datasource-funifier-docs/knowledge/modules/lastmile.md +568 -29
- package/datasource-funifier-docs/knowledge/modules/leaderboard.md +595 -126
- package/datasource-funifier-docs/knowledge/modules/level.md +536 -54
- package/datasource-funifier-docs/knowledge/modules/lottery.md +809 -76
- package/datasource-funifier-docs/knowledge/modules/marketplace.md +688 -17
- package/datasource-funifier-docs/knowledge/modules/mystery.md +662 -52
- package/datasource-funifier-docs/knowledge/modules/notification.md +564 -26
- package/datasource-funifier-docs/knowledge/modules/patterns.md +519 -814
- package/datasource-funifier-docs/knowledge/modules/player.md +773 -73
- package/datasource-funifier-docs/knowledge/modules/point.md +380 -83
- package/datasource-funifier-docs/knowledge/modules/public.md +508 -178
- package/datasource-funifier-docs/knowledge/modules/question.md +619 -99
- package/datasource-funifier-docs/knowledge/modules/quiz.md +565 -120
- package/datasource-funifier-docs/knowledge/modules/scheduler.md +1092 -39
- package/datasource-funifier-docs/knowledge/modules/security.md +674 -112
- package/datasource-funifier-docs/knowledge/modules/staging.md +742 -19
- package/datasource-funifier-docs/knowledge/modules/story.md +565 -29
- package/datasource-funifier-docs/knowledge/modules/studio-page.md +470 -144
- package/datasource-funifier-docs/knowledge/modules/swap.md +552 -84
- package/datasource-funifier-docs/knowledge/modules/team.md +563 -45
- package/datasource-funifier-docs/knowledge/modules/trigger.md +876 -134
- package/datasource-funifier-docs/knowledge/modules/upload.md +468 -95
- package/datasource-funifier-docs/knowledge/modules/virtual-good.md +510 -63
- package/datasource-funifier-docs/knowledge/modules/webhook.md +375 -28
- package/datasource-funifier-docs/knowledge/modules/websocket.md +459 -26
- package/datasource-funifier-docs/knowledge/modules/widget.md +613 -27
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +42 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/init.test.js +74 -3
- package/dist/cli/init.test.js.map +1 -1
- package/dist/cli/persona.d.ts +3 -0
- package/dist/cli/persona.d.ts.map +1 -0
- package/dist/cli/persona.js +25 -0
- package/dist/cli/persona.js.map +1 -0
- package/dist/core/api-client.d.ts +21 -1
- package/dist/core/api-client.d.ts.map +1 -1
- package/dist/core/api-client.js +154 -1
- package/dist/core/api-client.js.map +1 -1
- package/dist/core/constants.d.ts +14 -0
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/constants.js +14 -0
- package/dist/core/constants.js.map +1 -1
- package/dist/core/types/Folder.d.ts +16 -0
- package/dist/core/types/Folder.d.ts.map +1 -0
- package/dist/core/types/Folder.js +3 -0
- package/dist/core/types/Folder.js.map +1 -0
- package/dist/core/types/FolderContent.d.ts +10 -0
- package/dist/core/types/FolderContent.d.ts.map +1 -0
- package/dist/core/types/FolderContent.js +3 -0
- package/dist/core/types/FolderContent.js.map +1 -0
- package/dist/core/types/FolderContentType.d.ts +10 -0
- package/dist/core/types/FolderContentType.d.ts.map +1 -0
- package/dist/core/types/FolderContentType.js +3 -0
- package/dist/core/types/FolderContentType.js.map +1 -0
- package/dist/core/types/FolderLog.d.ts +11 -0
- package/dist/core/types/FolderLog.d.ts.map +1 -0
- package/dist/core/types/FolderLog.js +3 -0
- package/dist/core/types/FolderLog.js.map +1 -0
- package/dist/core/types/index.d.ts +4 -0
- package/dist/core/types/index.d.ts.map +1 -1
- package/dist/core/types/index.js +4 -0
- package/dist/core/types/index.js.map +1 -1
- package/dist/mcp/bundle.js +121 -87
- package/dist/mcp/check-update.d.ts +2 -0
- package/dist/mcp/check-update.d.ts.map +1 -0
- package/dist/mcp/check-update.js +44 -0
- package/dist/mcp/check-update.js.map +1 -0
- package/dist/mcp/index.js +5 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/resources/documentation.d.ts +1 -1
- package/dist/mcp/resources/documentation.d.ts.map +1 -1
- package/dist/mcp/resources/documentation.js +39 -3
- package/dist/mcp/resources/documentation.js.map +1 -1
- package/dist/mcp/tools/_char-guard.js +1 -1
- package/dist/mcp/tools/_char-guard.js.map +1 -1
- package/dist/mcp/tools/_fetch-current.d.ts +1 -1
- package/dist/mcp/tools/_fetch-current.d.ts.map +1 -1
- package/dist/mcp/tools/_fetch-current.js +12 -0
- package/dist/mcp/tools/_fetch-current.js.map +1 -1
- package/dist/mcp/tools/connect.d.ts.map +1 -1
- package/dist/mcp/tools/connect.js +18 -8
- package/dist/mcp/tools/connect.js.map +1 -1
- package/dist/mcp/tools/database.d.ts.map +1 -1
- package/dist/mcp/tools/database.js +59 -47
- package/dist/mcp/tools/database.js.map +1 -1
- package/dist/mcp/tools/database.test.js +2 -2
- package/dist/mcp/tools/database.test.js.map +1 -1
- package/dist/mcp/tools/delete.d.ts.map +1 -1
- package/dist/mcp/tools/delete.js +33 -3
- package/dist/mcp/tools/delete.js.map +1 -1
- package/dist/mcp/tools/execute.d.ts.map +1 -1
- package/dist/mcp/tools/execute.js +20 -9
- package/dist/mcp/tools/execute.js.map +1 -1
- package/dist/mcp/tools/folder.d.ts +4 -0
- package/dist/mcp/tools/folder.d.ts.map +1 -0
- package/dist/mcp/tools/folder.js +68 -0
- package/dist/mcp/tools/folder.js.map +1 -0
- package/dist/mcp/tools/get.d.ts.map +1 -1
- package/dist/mcp/tools/get.js +16 -6
- package/dist/mcp/tools/get.js.map +1 -1
- package/dist/mcp/tools/index.d.ts +1 -1
- package/dist/mcp/tools/index.d.ts.map +1 -1
- package/dist/mcp/tools/index.js +5 -1
- package/dist/mcp/tools/index.js.map +1 -1
- package/dist/mcp/tools/list.d.ts.map +1 -1
- package/dist/mcp/tools/list.js +38 -14
- package/dist/mcp/tools/list.js.map +1 -1
- package/dist/mcp/tools/logs.d.ts.map +1 -1
- package/dist/mcp/tools/logs.js +15 -5
- package/dist/mcp/tools/logs.js.map +1 -1
- package/dist/mcp/tools/save.d.ts.map +1 -1
- package/dist/mcp/tools/save.js +26 -4
- package/dist/mcp/tools/save.js.map +1 -1
- package/dist/mcp/tools/save.test.js +192 -1
- package/dist/mcp/tools/save.test.js.map +1 -1
- package/dist/mcp/tools/search-docs.d.ts +3 -0
- package/dist/mcp/tools/search-docs.d.ts.map +1 -0
- package/dist/mcp/tools/search-docs.js +102 -0
- package/dist/mcp/tools/search-docs.js.map +1 -0
- package/package.json +6 -2
- package/skills/acquire-funifier-knowledge/SKILL.md +132 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CONCERNS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_ENDPOINTS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/CUSTOM_PAGES.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/GAME_MECHANICS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/INTEGRATIONS.md +35 -0
- package/skills/acquire-funifier-knowledge/assets/templates/LEADERBOARDS.md +24 -0
- package/skills/acquire-funifier-knowledge/assets/templates/OVERVIEW.md +47 -0
- package/skills/acquire-funifier-knowledge/assets/templates/PLAYER_MODEL.md +31 -0
- package/skills/acquire-funifier-knowledge/assets/templates/SCHEDULERS.md +25 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TECHNIQUES_AND_PATTERNS.md +26 -0
- package/skills/acquire-funifier-knowledge/assets/templates/TRIGGERS.md +27 -0
- package/skills/acquire-funifier-knowledge/references/funifier-inventory-checklist.md +81 -0
- package/skills/acquire-funifier-knowledge/references/game-techniques-taxonomy.md +62 -0
- package/skills/acquire-funifier-knowledge/references/mcp-call-patterns.md +118 -0
- package/skills/funifier/SKILL.md +88 -0
- package/skills/funifier/references/configure-security.md +96 -0
- package/skills/{funifier-create-action/SKILL.md → funifier/references/create-action.md} +0 -33
- package/skills/funifier/references/create-aggregate.md +144 -0
- package/skills/funifier/references/create-challenge.md +116 -0
- package/skills/funifier/references/create-competition.md +98 -0
- package/skills/funifier/references/create-crossword.md +574 -0
- package/skills/funifier/references/create-custom-object.md +91 -0
- package/skills/funifier/references/create-custom-page.md +135 -0
- package/skills/funifier/references/create-folder.md +104 -0
- package/skills/funifier/references/create-lastmile.md +643 -0
- package/skills/{funifier-create-leaderboard/SKILL.md → funifier/references/create-leaderboard.md} +0 -33
- package/skills/funifier/references/create-level.md +94 -0
- package/skills/funifier/references/create-lottery.md +913 -0
- package/skills/funifier/references/create-mystery.md +769 -0
- package/skills/funifier/references/create-notification.md +75 -0
- package/skills/{funifier-create-point/SKILL.md → funifier/references/create-point.md} +0 -33
- package/skills/funifier/references/create-quiz.md +98 -0
- package/skills/funifier/references/create-scheduler.md +141 -0
- package/skills/funifier/references/create-story.md +636 -0
- package/skills/funifier/references/create-swap.md +95 -0
- package/skills/{funifier-create-trigger/SKILL.md → funifier/references/create-trigger.md} +0 -33
- package/skills/funifier/references/create-virtual-good.md +96 -0
- package/skills/funifier/references/create-webhook.md +72 -0
- package/skills/funifier/references/create-websocket.md +71 -0
- package/skills/funifier/references/create-widget.md +76 -0
- package/skills/funifier/references/debug.md +87 -0
- package/skills/funifier/references/help.md +81 -0
- package/skills/funifier/references/implement-frontend.md +106 -0
- package/skills/funifier/references/import-csv.md +75 -0
- package/skills/funifier/references/manage-player.md +82 -0
- package/skills/funifier/references/manage-team.md +76 -0
- package/skills/funifier/references/upload-file.md +91 -0
- package/datasource-funifier-docs/.search-index.json +0 -17318
- package/datasource-funifier-docs/.skills-map.json +0 -73
- package/skills/funifier-create-aggregate/SKILL.md +0 -127
- package/skills/funifier-create-challenge/SKILL.md +0 -88
- package/skills/funifier-create-custom-page/SKILL.md +0 -127
- package/skills/funifier-create-level/SKILL.md +0 -87
- package/skills/funifier-create-quiz/SKILL.md +0 -87
- package/skills/funifier-create-scheduler/SKILL.md +0 -127
- package/skills/funifier-create-virtual-good/SKILL.md +0 -87
- package/skills/funifier-debug/SKILL.md +0 -92
- package/skills/funifier-help/SKILL.md +0 -86
- package/skills/funifier-implement-frontend/SKILL.md +0 -90
- package/skills/funifier-index/SKILL.md +0 -58
|
@@ -1,104 +1,753 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `auth`
|
|
2
2
|
|
|
3
|
-
**Acesso Studio:** `/studio/
|
|
3
|
+
**Acesso Studio:** `/studio/security` _(rota de frontend — não verificável neste repositório)_
|
|
4
4
|
**API Endpoint:** `/v3/auth`
|
|
5
|
+
**Coleções MongoDB:** `auth_module` (gerenciada por este endpoint), `authentication` (legada v1/v2). Lê — sem gravar — `security` e `principal`.
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
> Documentação de engenharia reversa do módulo `auth` do projeto `funifier-service`. Baseada exclusivamente no comportamento real do código-fonte (pacotes `com.funifier.engine.auth.*`, `com.funifier.rest.v3.*`). Cada comportamento não óbvio cita `arquivo:linha`.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
---
|
|
9
10
|
|
|
10
|
-
##
|
|
11
|
+
## 1. Visão Geral
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
- Para gerar tokens de acesso
|
|
14
|
-
- Para implementar SSO com sistemas corporativos
|
|
15
|
-
- Para criar validações customizadas de autenticação
|
|
13
|
+
O módulo `auth` tem duas responsabilidades distintas, ambas servidas pela classe `AuthenticationRest` (`@Path("v3/auth")`, `rest/v3/rest/AuthenticationRest.java:52`):
|
|
16
14
|
|
|
17
|
-
|
|
15
|
+
1. **Emissão de tokens** (`POST /v3/auth/token`): implementação parcial de OAuth 2.0 (RFC6749) que autentica jogadores/aplicações e emite JWTs Bearer. Suporta `client_credentials`, `password` e `refresh_token`.
|
|
16
|
+
2. **CRUD de Auth Modules** (`/v3/auth/module`): cadastro de scripts Groovy customizados que rodam no login do jogador (PAM stacking — login por CPF, SSO externo, senha mestre, etc.).
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
- [ ] Configurar auto-criação de usuários
|
|
21
|
-
- [ ] Definir tempo de expiração do token
|
|
22
|
-
- [ ] Configurar módulo de autenticação customizado (se necessário)
|
|
18
|
+
A **autorização** de todas as requisições `/v3/*` acontece no `SecurityFilter` (`rest/v3/filter/SecurityFilter.java:27`), um servlet filter que valida o token e o escopo antes de liberar o acesso. O `SecurityFilter` é descrito aqui porque é o consumidor em runtime dos tokens emitidos pelo `/v3/auth/token`.
|
|
23
19
|
|
|
24
|
-
|
|
20
|
+
A lógica de autenticação de jogador vive em `com.funifier.engine.auth.AuthenticationManager` (`engine/auth/AuthenticationManager.java:37`), instanciada por gamificação via `FrontController.getInstance(apiKey).getManagerFactory()`.
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
> **Escopo deste documento:** cobre exclusivamente a autenticação de **jogadores** (`/v3/auth`). Subsistemas correlatos — `/v3/system/auth/module` (auth modules de usuário Studio), `/v3/sso/saml/*` (SSO de administradores Studio) e 2FA (usuários de conta) — são explicitamente delimitados na seção 7.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 2. Arquitetura e Fluxos
|
|
27
|
+
|
|
28
|
+
### 2.1 Pipeline principal — `POST /v3/auth/token`
|
|
29
|
+
|
|
30
|
+
O mesmo path aceita dois `Content-Type` em métodos diferentes:
|
|
31
|
+
|
|
32
|
+
- `application/x-www-form-urlencoded` → `AuthenticationRest.token()` (`:88`)
|
|
33
|
+
- `application/json` → `AuthenticationRest.tokenJson()` (`:232`)
|
|
34
|
+
|
|
35
|
+
Os dois métodos **não são equivalentes** (ver 2.4). Sequência do `token()` (form):
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
POST /v3/auth/token
|
|
39
|
+
1. [Roteamento por grant_type] → request.getParameter("grant_type") token():98
|
|
40
|
+
2a. client_credentials:
|
|
41
|
+
isAppSecretAllowed(client_secret) AuthenticationManager:189
|
|
42
|
+
se inválido → 401 UNAUTHORIZED_CLIENT token():108
|
|
43
|
+
findApplication(secret) → app.scope, app.whitelist token():111-112
|
|
44
|
+
accessToken(scale=DAY, amount=7) + refreshToken (1 ano) token():112-113
|
|
45
|
+
2b. password:
|
|
46
|
+
authenticateModules(req, m, context) token():133 → AuthenticationManager:693
|
|
47
|
+
moduleStatus==2 → 401 auth_module_failed (se houver output) ou 400 invalid_grant token():135-144
|
|
48
|
+
username = req.getParameter("username") ← pode ter sido reescrito por módulo token():148
|
|
49
|
+
moduleStatus==1 OU (moduleStatus==0 && authenticatePassword(player,password)) token():150
|
|
50
|
+
findRoleCalculatedToPlayer(player) → scope, expiration, whitelist token():151-155
|
|
51
|
+
accessToken(scope, expiration, role.whitelist) + refreshToken token():155-156
|
|
52
|
+
2c. refresh_token:
|
|
53
|
+
accessTokenWithRefreshToken(refresh_token) token():171 → TokenUtil:80
|
|
54
|
+
gera novo refresh_token (rotativo) token():181
|
|
55
|
+
2d. qualquer outro grant_type → 400 invalid_grant token():187
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Algoritmo de decisão dos Auth Modules** (`AuthenticationManager.authenticateModules`, `:693`):
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
status = 0; ok_sufficient = 0; ok_required = 0; tot_required = 0
|
|
62
|
+
para cada AuthModule ordenado por "order" asc (findAll():685):
|
|
63
|
+
se relevance == "required": tot_required++
|
|
64
|
+
se limite diário de execução de auth (newTriggerExecution) excedido:
|
|
65
|
+
loga e PULA o módulo (não falha) :763-765
|
|
66
|
+
senão:
|
|
67
|
+
result = AuthRunner.run(module, ...) :729
|
|
68
|
+
se result.userid == null E relevance == "requisite":
|
|
69
|
+
return status = 2 (falha imediata) :733-736
|
|
70
|
+
se result.userid != null:
|
|
71
|
+
se required → ok_required++
|
|
72
|
+
se sufficient → ok_sufficient++ :739-746
|
|
73
|
+
se o módulo lançar exceção: e.printStackTrace() e CONTINUA :767-770
|
|
74
|
+
fim-para
|
|
75
|
+
se ok_sufficient > 0: status = 1 :774
|
|
76
|
+
senão se tot_required != ok_required: status = 2 :778
|
|
77
|
+
senão: status = 0
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- `status=0` → não houve `sufficient`; segue para `authenticatePassword` (auth Funifier padrão).
|
|
81
|
+
- `status=1` → ao menos um `sufficient` retornou userid; **pula** a validação de senha Funifier.
|
|
82
|
+
- `status=2` → falha; o token **não** é emitido.
|
|
83
|
+
|
|
84
|
+
### 2.2 Fluxo de emissão de token (visão geral)
|
|
85
|
+
|
|
86
|
+
### Fluxo — POST /v3/auth/token (form-urlencoded)
|
|
87
|
+
|
|
88
|
+
```mermaid
|
|
89
|
+
flowchart TD
|
|
90
|
+
A[POST /v3/auth/token] --> B{grant_type?}
|
|
91
|
+
B -->|client_credentials| C[isAppSecretAllowed]
|
|
92
|
+
B -->|password| D[authenticateModules - PAM]
|
|
93
|
+
B -->|refresh_token| E[accessTokenWithRefreshToken]
|
|
94
|
+
B -->|outro| X[400 invalid_grant]
|
|
95
|
+
|
|
96
|
+
C -->|false| Y[401 unauthorized_client]
|
|
97
|
+
C -->|true| F[findApplication: scope + whitelist]
|
|
98
|
+
F --> T[accessToken DAY,7 + refreshToken 1 ano]
|
|
99
|
+
|
|
100
|
+
D --> G{moduleStatus}
|
|
101
|
+
G -->|2 + output| Z[401 auth_module_failed]
|
|
102
|
+
G -->|2| X
|
|
103
|
+
G -->|1| H[findRoleCalculatedToPlayer]
|
|
104
|
+
G -->|0| I[authenticatePassword]
|
|
105
|
+
I -->|lança AuthenticationException| X
|
|
106
|
+
I -->|true| H
|
|
107
|
+
H --> T2[accessToken scope/expiration/whitelist + refreshToken]
|
|
108
|
+
|
|
109
|
+
E --> T3[novo access_token + novo refresh_token]
|
|
110
|
+
T --> OK[200 Bearer]
|
|
111
|
+
T2 --> OK
|
|
112
|
+
T3 --> OK
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 2.3 PAM Stacking — execução dos Auth Modules
|
|
116
|
+
|
|
117
|
+
### Sequência — authenticateModules
|
|
118
|
+
|
|
119
|
+
```mermaid
|
|
120
|
+
sequenceDiagram
|
|
121
|
+
participant R as AuthenticationRest.token
|
|
122
|
+
participant M as AuthenticationManager
|
|
123
|
+
participant Run as AuthRunner
|
|
124
|
+
participant G as Script Groovy
|
|
125
|
+
|
|
126
|
+
R->>M: authenticateModules(req, manager, context)
|
|
127
|
+
loop cada AuthModule (order asc)
|
|
128
|
+
M->>Run: run(module, req, manager, context)
|
|
129
|
+
Run->>Run: compila/recupera classe do cache
|
|
130
|
+
Run->>G: invokeMethod("authenticate", req)
|
|
131
|
+
G-->>Run: userid (String) ou null
|
|
132
|
+
Run-->>M: {userid, err, out, millis}
|
|
133
|
+
alt requisite e userid == null
|
|
134
|
+
M-->>R: status = 2 (falha imediata)
|
|
135
|
+
else sufficient e userid != null
|
|
136
|
+
M->>M: ok_sufficient++
|
|
137
|
+
else required e userid != null
|
|
138
|
+
M->>M: ok_required++
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
M-->>R: AuthModulesResult(status 0/1/2)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 2.4 Diferenças entre `token()` (form) e `tokenJson()` (JSON)
|
|
145
|
+
|
|
146
|
+
`tokenJson()` (`:232`) é uma reimplementação **mais fraca** do que `token()`. Diferenças confirmadas no código:
|
|
147
|
+
|
|
148
|
+
| Aspecto | `token()` form (`:88`) | `tokenJson()` JSON (`:232`) |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| `grant_type=refresh_token` | Suportado (`:167`) | **Não implementado** (cai no `else` → 400, `:311`) |
|
|
151
|
+
| Emite `refresh_token` | Sim (`:113`, `:156`) | **Não** — nenhuma resposta inclui refresh_token |
|
|
152
|
+
| Claim `whitelist` no token | Sim, propaga `app.whitelist`/`role.whitelist` (`:112`, `:155`) | **Não** — chama `accessToken(...)` sem whitelist (`:262`, `:300`) |
|
|
153
|
+
| Chave do apiKey | `apiKey` | `client_id` (cc) / `apiKey` (password) |
|
|
154
|
+
|
|
155
|
+
**Consequência de segurança:** tokens emitidos pelo endpoint JSON **não carregam a claim `whitelist`**. Como o `SecurityFilter` só valida whitelist quando a claim existe (`SecurityFilter:189`), tokens minteados via JSON **ignoram silenciosamente** a restrição de IP/domínio configurada na Application/Role.
|
|
156
|
+
|
|
157
|
+
### 2.5 Pipeline — `SecurityFilter` (autorização de toda requisição `/v3/*`)
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
doFilter():
|
|
161
|
+
1. método OPTIONS → passa direto (preflight CORS) SecurityFilter:75
|
|
162
|
+
2. apikey = AuthBean.getApiKey(Authorization) :81
|
|
163
|
+
3. entity = path sem "/v3/" (primeiro segmento) :84-88
|
|
164
|
+
entity_full = path sem "/v3/" com "/" → "_"
|
|
165
|
+
4. rate limit: StatisticManager.newApiRequest(method, entity) :98
|
|
166
|
+
se estourou → 401 "You have exceeded your gamification daily api requests limits" :100
|
|
167
|
+
5. paths públicos (lista hardcoded) → passa direto :107-150
|
|
168
|
+
6. detecção do tipo de auth via header (usa .contains()): :152-187
|
|
169
|
+
Basic → authPublic / authBasic / findApplication → scope + whitelist
|
|
170
|
+
Account → AccountManager.isAppSecretAllowed (NÃO define scope)
|
|
171
|
+
Studio → TokenUtil.getValueStudio (NÃO define scope)
|
|
172
|
+
Bearer → TokenUtil.getValue(...,"apiKey"|"account"); scope + whitelist do token
|
|
173
|
+
7. se whitelist != null → isRequestWhiteListed(referer/origin/ip) :189-197
|
|
174
|
+
8. se scope != null → verificação de escopo vs método/entity :200-265
|
|
175
|
+
9. erro → 401 com mensagem específica; senão → filter.doFilter() :267-292
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Mapeamento método→ação de escopo** (`SecurityFilter:205`): `GET→read`, `POST→write`, `PUT→write`, **demais → método em minúsculas** (logo `DELETE→delete`). A liberação ocorre se o escopo contém `{m}_all`, ou a lista contém `{m}_{entity_full}`, ou `{m}_{full_menos_1}_all`, ou `{m}_{full_menos_2}_all` (`:209-235`).
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 3. Estrutura dos Objetos
|
|
183
|
+
|
|
184
|
+
### 3.1 `AuthModule` — módulo customizado (coleção: `auth_module`)
|
|
185
|
+
|
|
186
|
+
Mapeado em `engine/auth/AuthModule.java`. Coleção definida em `Entity.AUTH_MODULE` → `"auth_module"` (`util/Entity.java:253`).
|
|
187
|
+
|
|
188
|
+
| Campo | Tipo | Padrão | Obrigatório | Descrição |
|
|
189
|
+
|---|---|---|---|---|
|
|
190
|
+
| `_id` | String | `Guid.newShortGuid()` se ausente | não | Id; gerado no `add()` se nulo/vazio (`AuthenticationManager:659`) |
|
|
191
|
+
| `name` | String | null | não | Nome descritivo |
|
|
192
|
+
| `script` | String | — | sim | Código Groovy com método `authenticate(request)` |
|
|
193
|
+
| `relevance` | String | null | sim | `required`, `requisite`, `sufficient`, `optional` |
|
|
194
|
+
| `order` | int | `0` | não | Ordem de execução ASC (`findAll().sort("{order:1}")`, `:687`) |
|
|
195
|
+
| `created` | Date | `new Date()` na 1ª gravação | não | Definido apenas se nulo (`:660`) |
|
|
196
|
+
| `updated` | Date | `new Date()` a cada `add()` | não | Sempre sobrescrito no save (`:663`) |
|
|
197
|
+
| `timeout` | Long | `5` (segundos) | não | Timeout do `future.get` (`AuthRunner:229-232`) — ver 6.3 |
|
|
198
|
+
|
|
199
|
+
**Constantes de `relevance`** (`AuthModule.java:12-15`) e comportamento PAM real (`authenticateModules`):
|
|
200
|
+
|
|
201
|
+
| Valor | Comportamento confirmado no código |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `requisite` | Se `userid == null` → falha **imediata** (`status=2`, return) (`:733`). Se sucesso, **não** é contabilizado em `ok_required`/`ok_sufficient`. |
|
|
204
|
+
| `sufficient` | Se `userid != null` → `ok_sufficient++`; ao final, qualquer `ok_sufficient>0` ⇒ `status=1` (pula senha Funifier) (`:743`,`:774`). |
|
|
205
|
+
| `required` | Conta em `tot_required`; sucesso ⇒ `ok_required++`. Se ao final `tot_required != ok_required` (e sem sufficient) ⇒ `status=2` (`:720`,`:740`,`:778`). |
|
|
206
|
+
| `optional` | **Resultado totalmente ignorado** — não há ramo para `optional` em `authenticateModules`. |
|
|
207
|
+
|
|
208
|
+
> **Comportamento silencioso:** um módulo `requisite` que **lança exceção** (em vez de retornar `null`) é capturado por `catch(Exception)` com apenas `printStackTrace()` (`:767-770`) e o loop **continua** — não dispara `status=2`. Só o retorno `null` de um `requisite` causa falha imediata.
|
|
209
|
+
|
|
210
|
+
### 3.2 `Authentication` — sessão legada (coleção: `authentication`)
|
|
211
|
+
|
|
212
|
+
Mapeado em `engine/auth/Authentication.java`. **Não é usado pelo `/v3/auth/token`** — o JWT v3 é stateless. Esta coleção só é lida/gravada pelos métodos legados v1/v2 (`authenticate(playerId)`, `logout`, `isAuthenticated`).
|
|
213
|
+
|
|
214
|
+
| Campo | Tipo | Padrão | Descrição |
|
|
215
|
+
|---|---|---|---|
|
|
216
|
+
| `_id` (`access_token`) | String | `Guid.newShortGuid()` | Token de sessão legado |
|
|
217
|
+
| `api_key` | String | null | API key da gamificação |
|
|
218
|
+
| `auth_mode` | String | null | IMPLICIT, PASSWORD, CREDENTIAL, FACEBOOK, GOOGLE |
|
|
219
|
+
| `player` | String | — | Id do jogador |
|
|
220
|
+
| `expires_in` | Date | null | Expiração da sessão |
|
|
221
|
+
| `language` | String | "en-US" | Idioma |
|
|
222
|
+
| `host` | String | null | Host de origem |
|
|
223
|
+
| `developer` | boolean | false | Permissão de edição inline |
|
|
224
|
+
|
|
225
|
+
**Campos removidos/legados (comentados no código, `Authentication.java:18-20`):** `ip_adress` (typo no original), `session`, `userId`.
|
|
226
|
+
|
|
227
|
+
### 3.3 `Security` — configuração da gamificação (coleção: `security`)
|
|
228
|
+
|
|
229
|
+
Mapeado em `engine/security/Security.java`. **Documento único por gamificação** — `SecurityDaoMongo.update()` faz `c.remove()` (todos) e depois `save()` (`SecurityDaoMongo.java:11`). Gerenciado pelo endpoint `/v3/security`, **não** por `/v3/auth`. O `auth` apenas **lê** este documento.
|
|
230
|
+
|
|
231
|
+
| Campo | Tipo | Padrão | Descrição |
|
|
232
|
+
|---|---|---|---|
|
|
233
|
+
| `_id` | String | — | Id |
|
|
234
|
+
| `created_at` / `updated_at` | Date | null | Timestamps |
|
|
235
|
+
| `requirePassword` | boolean | `false` | Se `false`, BCrypt **não** é exigido (ver 5.1) |
|
|
236
|
+
| `createPlayerIfDontExist` | boolean | **`true`** | Auto-cria jogador no 1º login (`Security.java:18`) |
|
|
237
|
+
| `timeZone` | String | null | Fuso da gamificação |
|
|
238
|
+
| `inlineEditable` | boolean | false | Edição inline no Studio |
|
|
239
|
+
| `appIdFacebook` / `appSecretFacebook` | String | null | Credenciais Facebook (só fluxo legado) |
|
|
240
|
+
| `appIdGoogle` / `appSecretGoogle` | String | null | Credenciais Google (só fluxo legado) |
|
|
241
|
+
| `appIdTwitter` / `appSecretTwitter` | String | null | Campos presentes, **sem implementação** |
|
|
242
|
+
| `apps` | List\<Application\> | `[]` | Aplicações (app_secret + scope) |
|
|
243
|
+
| `roles` | List\<Role\> | `[]` | Roles (timeout + scope) |
|
|
244
|
+
| `websites` | List\<String\> | `[]` | Whitelist de domínios (modo IMPLICIT legado) |
|
|
245
|
+
| `runInSilentMode` | boolean | false | Modo silencioso |
|
|
246
|
+
| `baselines` | List\<BaseLine\> | `[]` | Configurações baseline |
|
|
247
|
+
| `homepage` | String | null | Página inicial no Studio |
|
|
248
|
+
| `ldap` | LDAPConfig | null | Config LDAP (implementação comentada — ver 7) |
|
|
249
|
+
|
|
250
|
+
> **Default ausente = ainda autentica.** `SecurityDaoMongo.find()` retorna `new Security()` quando **não existe** documento (`SecurityDaoMongo.java:18`). Como o construtor default deixa `createPlayerIfDontExist=true` e `requirePassword=false`, uma gamificação sem documento `security` **autentica qualquer jogador sem senha e o cria**.
|
|
251
|
+
|
|
252
|
+
### 3.4 `Application` — app registrado (embutido em `security.apps`)
|
|
253
|
+
|
|
254
|
+
`engine/security/Application.java`.
|
|
255
|
+
|
|
256
|
+
| Campo | Tipo | Descrição |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| `name` | String | Nome descritivo |
|
|
259
|
+
| `app_secret` | String | Secret para Basic/`client_credentials` |
|
|
260
|
+
| `scope` | String | Escopos separados por vírgula |
|
|
261
|
+
| `whitelist` | List\<String\> | IPs/domínios autorizados |
|
|
262
|
+
|
|
263
|
+
**Constantes de escopo (`Application.java:7-19`)** — valores literais usados pelo `SecurityFilter`:
|
|
264
|
+
|
|
265
|
+
| Constante | Valor | Significado |
|
|
266
|
+
|---|---|---|
|
|
267
|
+
| `SCOPE_READ_ALL` | `read_all` | Ler qualquer entidade |
|
|
268
|
+
| `SCOPE_WRITE_ALL` | `write_all` | Escrever qualquer entidade |
|
|
269
|
+
| `SCOPE_DELETE_ALL` | `delete_all` | Deletar qualquer entidade |
|
|
270
|
+
| `SCOPE_DATABASE` | `database` | Acesso ao endpoint `/v3/database` |
|
|
271
|
+
| `SCOPE_WRITE_ACTIONLOG` | `write_actionlog` | Escrever em `/v3/action/log` (tratamento especial, `SecurityFilter:238`) |
|
|
272
|
+
| `SCOPE_WRITE_UPLOAD` | `write_upload` | Upload de arquivos |
|
|
273
|
+
| `SCOPE_READ_CRYPTED` | `read_encrypted_field_values` | Ler campos criptografados |
|
|
274
|
+
| `SCOPE_READ_PLAYER_PASSWORD` | `read_encrypted_player_password` | Ler senha do player |
|
|
275
|
+
| `SCOPE_CROSSDOMAIN` | `cross_domain` | Excluir gamifications/accounts de outras contas |
|
|
276
|
+
|
|
277
|
+
### 3.5 `Role` — role de jogador (embutido em `security.roles`)
|
|
278
|
+
|
|
279
|
+
`engine/security/Role.java`. **Diferente** de `com.funifier.engine.system.role.Role` (roles de usuário Studio).
|
|
280
|
+
|
|
281
|
+
| Campo | Tipo | Descrição |
|
|
282
|
+
|---|---|---|
|
|
283
|
+
| `name` | String | Identificador (ex.: `player`, `public`) |
|
|
284
|
+
| `timeout` | String | Ex.: `7d`, `1h`, `30m` — interpretado por `DateUtil.fromKeyword("+"+timeout)` |
|
|
285
|
+
| `scope` | String | Escopos separados por vírgula |
|
|
286
|
+
| `whitelist` | List\<String\> | IPs/domínios autorizados |
|
|
287
|
+
|
|
288
|
+
### 3.6 `Principal` — vínculo player↔roles (coleção: `principal`)
|
|
289
|
+
|
|
290
|
+
`engine/player/Principal.java`. Lido por `findRoleCalculatedToPlayer` para descobrir as roles do jogador.
|
|
291
|
+
|
|
292
|
+
| Campo | Tipo | Descrição |
|
|
293
|
+
|---|---|---|
|
|
294
|
+
| `_id` | String | Id do principal |
|
|
295
|
+
| `name` | String | Nome |
|
|
296
|
+
| `type` | Integer | `0`=USER, `1`=TEAM (`TYPE_USER`/`TYPE_TEAM`) |
|
|
297
|
+
| `teamId` / `userId` | String | Referência conforme o tipo |
|
|
298
|
+
| `roles` | List\<String\> | Nomes das roles do jogador |
|
|
299
|
+
|
|
300
|
+
> **Importante:** jogadores criados pelo `/v3/auth/token` (password) **não recebem Principal** (ver 5.2). Logo `principal` é `null` para eles e o cálculo de role cai no ramo "sem principal".
|
|
301
|
+
|
|
302
|
+
### 3.7 JWT — estrutura interna dos tokens
|
|
303
|
+
|
|
304
|
+
Gerados em `TokenUtil` com `io.jsonwebtoken` (jjwt), assinatura **HS512**, payload comprimido com **GZIP** (`TokenUtil.java:30-39`).
|
|
305
|
+
|
|
306
|
+
**access_token** (claims, `:30`,`:37`,`:137`,`:143`):
|
|
307
|
+
|
|
308
|
+
| Claim | Presente quando |
|
|
309
|
+
|---|---|
|
|
310
|
+
| `apiKey` | Sempre (gamificação) |
|
|
311
|
+
| `appSecret` | Apenas `client_credentials` |
|
|
312
|
+
| `player` | Apenas `password` |
|
|
313
|
+
| `scope` | Sempre |
|
|
314
|
+
| `account` / `user` | Tokens de conta / usuário Studio |
|
|
315
|
+
| `whitelist` | Apenas quando emitido pelo endpoint **form** com whitelist configurada |
|
|
316
|
+
|
|
317
|
+
**refresh_token** (`generateRefreshToken`, `:45`): claims **ofuscados** `apk`, `aps`, `pla`, `acc`, `usr`; validade **1 ano** (`TimeScale.YEAR, 1`); **sem escopo** ("este token nao tem acesso a nada").
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## 4. Endpoints
|
|
322
|
+
|
|
323
|
+
### `POST /v3/auth/token`
|
|
324
|
+
|
|
325
|
+
| Aspecto | Detalhe |
|
|
326
|
+
|---|---|
|
|
327
|
+
| Finalidade | Emitir access_token OAuth 2.0 |
|
|
328
|
+
| Autenticação | Nenhuma — path público (`SecurityFilter:112`) |
|
|
329
|
+
| Content-Type | `application/x-www-form-urlencoded` **ou** `application/json` (lógicas distintas — ver 2.4) |
|
|
330
|
+
|
|
331
|
+
`expires_in` na resposta é o **timestamp Unix em milissegundos** da expiração (`Long.toString(expiration.getTime())`), **não** segundos como o padrão OAuth2.
|
|
332
|
+
|
|
333
|
+
#### grant_type=client_credentials
|
|
334
|
+
|
|
335
|
+
| Param | Descrição |
|
|
336
|
+
|---|---|
|
|
337
|
+
| `client_id` | API key da gamificação |
|
|
338
|
+
| `client_secret` | App secret cadastrado em `security.apps` |
|
|
339
|
+
| `grant_type` | `client_credentials` |
|
|
340
|
+
|
|
341
|
+
**Comportamento real:** valida o secret (`isAppSecretAllowed`); usa `app.scope`; token válido por **7 dias fixos** (`TimeScale.DAY, 7`, `:112`). No endpoint **form**, emite `refresh_token` e propaga `app.whitelist`; no **JSON**, não emite refresh_token nem whitelist (`:262`). Secret inválido → **HTTP 401** (`unauthorized_client`).
|
|
29
342
|
|
|
30
|
-
**Exemplo de Body:**
|
|
31
343
|
```json
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
344
|
+
// Request (JSON)
|
|
345
|
+
{ "grant_type": "client_credentials", "client_id": "5a3f...567", "client_secret": "abc123xyz" }
|
|
346
|
+
// Response
|
|
347
|
+
{ "access_token": "eyJ...", "expires_in": "1748387200000", "token_type": "Bearer" }
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
#### grant_type=password
|
|
351
|
+
|
|
352
|
+
| Param | Descrição |
|
|
353
|
+
|---|---|
|
|
354
|
+
| `apiKey` | API key da gamificação |
|
|
355
|
+
| `username` | Id do jogador (pode ser reescrito por Auth Module — `:148`) |
|
|
356
|
+
| `password` | Senha (exigida só se `requirePassword=true` **e** enviada — ver 5.1) |
|
|
357
|
+
| `grant_type` | `password` |
|
|
358
|
+
|
|
359
|
+
**Comportamento real:**
|
|
360
|
+
1. Roda os Auth Modules (PAM). `status=2` ⇒ 401 `auth_module_failed` (com `output`) ou 400 `invalid_grant`.
|
|
361
|
+
2. Um Auth Module pode **substituir o `username`** via `request.setParameter("username", novo)` (`AuthRequest:65`); o valor reescrito é relido em `:148`.
|
|
362
|
+
3. `status=1` (sufficient) pula a validação de senha; `status=0` chama `authenticatePassword`.
|
|
363
|
+
4. Escopo/expiração derivam de `findRoleCalculatedToPlayer`; default quando não há role/scope: `read_all,write_actionlog,write_upload` e `+7d` (`:152-153`).
|
|
364
|
+
5. Form emite `refresh_token` (1 ano); JSON não.
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
// Request (JSON)
|
|
368
|
+
{ "grant_type": "password", "apiKey": "5a3f...567", "username": "tom", "password": "abc123" }
|
|
38
369
|
```
|
|
39
370
|
|
|
40
|
-
|
|
371
|
+
#### grant_type=refresh_token
|
|
372
|
+
|
|
373
|
+
> Disponível **apenas** via form-urlencoded. O `tokenJson` não implementa este branch (cai em 400, `:311`).
|
|
374
|
+
|
|
375
|
+
| Param | Descrição |
|
|
376
|
+
|---|---|
|
|
377
|
+
| `refresh_token` | Refresh token previamente emitido |
|
|
378
|
+
| `grant_type` | `refresh_token` |
|
|
379
|
+
|
|
380
|
+
**Comportamento:** decodifica os claims ofuscados, reconstrói o access_token com as mesmas credenciais (`TokenUtil.generateAccessTokenWithRefreshToken:80`) e emite um **novo** refresh_token rotativo (`:181`). Só reconstrói para `apiKey` (app ou player); refresh de `account`/`user` retorna `null` ⇒ 401.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
### `GET /v3/auth/module`
|
|
385
|
+
|
|
386
|
+
| Aspecto | Detalhe |
|
|
387
|
+
|---|---|
|
|
388
|
+
| Autenticação | Bearer (escopo via SecurityFilter) |
|
|
389
|
+
|
|
390
|
+
**Query params** (`findAuthModules:352` → `AuthenticationManager.findAll:791`):
|
|
391
|
+
|
|
392
|
+
| Param | Descrição |
|
|
393
|
+
|---|---|
|
|
394
|
+
| `id` | Filtro por `_id` exato |
|
|
395
|
+
| `q` | **Query Mongo inline injetada crua** na pipeline `$match` (`:809`) — ver 8 |
|
|
396
|
+
| `published_min` / `published_max` | Faixa de `created` (RFC3339 ou keyword `-1d`) |
|
|
397
|
+
| `orderby` | Campo de ordenação |
|
|
398
|
+
| `reverse` | `true` = desc |
|
|
399
|
+
| `max_results` | Limite (default **100** se ≤ 0, `:804`) |
|
|
400
|
+
|
|
401
|
+
A consulta é uma agregação MongoDB com `$match`, `$sort` e `$limit`.
|
|
402
|
+
|
|
403
|
+
### `GET /v3/auth/module/{id}`
|
|
404
|
+
Retorna um `AuthModule` por id (`find:324` → `:786`).
|
|
405
|
+
|
|
406
|
+
### `POST /v3/auth/module`
|
|
407
|
+
|
|
408
|
+
Compila o script Groovy **antes** de salvar (`insertAuthModule:399`). Em `CompilationFailedException | InstantiationException | IllegalAccessException`, retorna **HTTP 201** com `{"status":"ERROR","message":"..."}` e o módulo **não é salvo** (`:415`). Sucesso → `{"status":"OK","module":{...}}` (HTTP 201).
|
|
409
|
+
|
|
41
410
|
```json
|
|
42
411
|
{
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
412
|
+
"_id": "master_pass_auth_module",
|
|
413
|
+
"name": "Master Password Auth Module",
|
|
414
|
+
"relevance": "sufficient",
|
|
415
|
+
"order": 0,
|
|
416
|
+
"script": "String authenticate(request){ String p = request.getParameter(\"username\"); if(\"MASTER\".equals(request.getParameter(\"password\"))) return p; return null; }"
|
|
46
417
|
}
|
|
47
418
|
```
|
|
48
419
|
|
|
49
|
-
###
|
|
50
|
-
**
|
|
51
|
-
**Endpoint:** `/v3/auth/module`
|
|
420
|
+
### `DELETE /v3/auth/module/{id}`
|
|
421
|
+
Remove o módulo e limpa o cache de classe compilada em memória (`compiled.remove(id)`, `dates.remove(id)`, `:681-682`). Resposta **HTTP 204** sem body.
|
|
52
422
|
|
|
53
|
-
###
|
|
54
|
-
|
|
55
|
-
**Endpoint:** `/v3/auth/module`
|
|
423
|
+
### `GET /v3/auth/origin`
|
|
424
|
+
Endpoint de diagnóstico (`origin:447`). Retorna todos os headers de IP candidatos (`X-Forwarded-For`, `Proxy-Client-IP`, etc.), o IP resolvido e a URL (`referer`/`origin`). Não há tratamento explícito de auth no método; o acesso depende do `SecurityFilter`.
|
|
56
425
|
|
|
57
|
-
|
|
426
|
+
---
|
|
58
427
|
|
|
59
|
-
|
|
428
|
+
## 5. Regras de Negócio
|
|
60
429
|
|
|
61
|
-
|
|
62
|
-
- **Timeout:** tempo de expiração do token (ex: `1d` = 1 dia)
|
|
63
|
-
- **Scope:** lista de permissões separadas por vírgula
|
|
430
|
+
### 5.1 Validação de senha — `requirePassword` só atua quando há senha
|
|
64
431
|
|
|
65
|
-
|
|
432
|
+
Trecho literal de `authenticatePassword` (`AuthenticationManager.java:324`):
|
|
66
433
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
| `write_player` | Escrita no endpoint `/v3/player` |
|
|
73
|
-
| `write_actionlog` | Escrita no endpoint `/v3/action_log` |
|
|
74
|
-
| `write_upload` | Escrita no endpoint `/v3/upload` |
|
|
75
|
-
| `write_database_<collection>` | Escrita em uma coleção específica do database (ex: `write_database_task__c`) |
|
|
76
|
-
| `delete_database_<collection>` | Exclusão em uma coleção específica do database |
|
|
77
|
-
| `database` | **⚠️ OBRIGATÓRIO** para acessar o endpoint `/v3/database` — sem esta palavra no scope, o endpoint retorna dados vazios silenciosamente (sem erro) |
|
|
434
|
+
```java
|
|
435
|
+
else if (password != null && (s.isRequirePassword() && (user.getPassword() == null || !BCrypt.checkpw(password, user.getPassword())) )) {
|
|
436
|
+
error = new Message("password incorrect for player", Response.Status.UNAUTHORIZED);
|
|
437
|
+
}
|
|
438
|
+
```
|
|
78
439
|
|
|
79
|
-
|
|
440
|
+
Consequências reais:
|
|
441
|
+
- `requirePassword=false` → expressão inteira `false` → **qualquer senha (ou nenhuma) passa**. Intencional para SSO/auth externa.
|
|
442
|
+
- `requirePassword=true` **mas `password` ausente** (`null`) → o `password != null` curto-circuita em `false` → **nenhum erro → login passa sem senha**. Omitir o campo `password` contorna a exigência de senha.
|
|
443
|
+
- `requirePassword=true`, senha enviada, e o player **sem** senha armazenada (`getPassword()==null`) → erro `password incorrect for player`.
|
|
80
444
|
|
|
81
|
-
|
|
445
|
+
### 5.2 Auto-criação de jogador (sem Principal)
|
|
82
446
|
|
|
83
|
-
|
|
447
|
+
Se o player não existe e `createPlayerIfDontExist=true` (`:299-311`):
|
|
448
|
+
1. Verifica `Account.players_allowed` vs `findTotal()`; se atingiu o limite → `AuthenticationException` `create player X not allowed, because reach the limit ...`.
|
|
449
|
+
2. Cria `new Player(player, player, null, null, null, false, false)` — id = nome = username, demais campos nulos — e `insert()`.
|
|
450
|
+
3. **Não cria `Principal`.** (Diferente do método legado `authenticate(auth_mode,...)`, que cria Principal em `:443`.)
|
|
451
|
+
|
|
452
|
+
Como o player auto-criado não tem `Principal`, `findRoleCalculatedToPlayer` cai no ramo "sem principal" e usa `security.getRole("player")`.
|
|
453
|
+
|
|
454
|
+
### 5.3 Cálculo de role do jogador (`findRoleCalculatedToPlayer:202`)
|
|
455
|
+
|
|
456
|
+
Prioridade exata:
|
|
457
|
+
```
|
|
458
|
+
1. principal nulo OU sem roles → security.getRole("player")
|
|
459
|
+
2. existe role com name == player → essa role
|
|
460
|
+
3. principal tem exatamente 1 role → security.getRole(roles[0])
|
|
461
|
+
4. principal tem >1 role:
|
|
462
|
+
scope = concatenação de todos os scopes + ","
|
|
463
|
+
timeout = o que gera a MENOR data (sessão mais curta)
|
|
464
|
+
whitelist= união sem duplicatas; SE qualquer role tiver whitelist == null
|
|
465
|
+
→ whitelist final = null (acesso irrestrito)
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
> **Bug confirmado (NPE):** o resultado pode ser `null` em dois caminhos: (a) ramo 1, quando **não existe** a role `player`; (b) ramo 3, quando o principal tem exatamente 1 role mas o `name` dela **não existe** em `security.roles` (`security.getRole(...)` retorna `null`, `:221`). Em ambos, `scope`/`expiration` têm null-guard, mas `role.whitelist` é desreferenciado **sem guard** em `AuthenticationRest.token():155` e em `TokenUtil.generateAccessTokenWithRefreshToken:117` → `NullPointerException` → capturada pelo `catch(Exception)` do `token()` → **HTTP 400** com mensagem vazia. O endpoint **JSON** (`:300`) não passa `role.whitelist`, portanto não sofre o NPE (mas também não emite whitelist).
|
|
469
|
+
|
|
470
|
+
### 5.4 Isolamento por gamificação (multi-tenant)
|
|
471
|
+
|
|
472
|
+
Toda operação resolve `FrontController.getInstance(apiKey).getManagerFactory()`, instanciando o `ManagerFactory` da gamificação. Não há acesso cruzado no fluxo normal. A assinatura do JWT, porém, **não** é por-gamificação (ver 8.1).
|
|
473
|
+
|
|
474
|
+
### 5.5 Cadastro de app (`isAppAllowed`) — igualdade exata
|
|
475
|
+
|
|
476
|
+
`SecurityDaoMongo.isAppAllowed` (`:55`) retorna `true` somente se `count("{apps.app_secret : #}") == 1`. Se **dois** apps compartilharem o mesmo `app_secret`, `count==2` → retorna `false` → autenticação Basic/client_credentials **falha** para esse secret.
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## 6. Comportamentos Automáticos
|
|
481
|
+
|
|
482
|
+
| Comportamento | Trigger | Impacto | Persistência |
|
|
483
|
+
|---|---|---|---|
|
|
484
|
+
| Auto-criação de Player | Login `password` com player inexistente e `createPlayerIfDontExist=true` | `Player` zerado inserido em `player` (sem Principal) | Permanente |
|
|
485
|
+
| `_id`/`created`/`updated` no AuthModule | `add(module)` | Gera `_id` se vazio, `created` se nulo, `updated` sempre | Permanente |
|
|
486
|
+
| Cache de classe Groovy | 1º uso de um AuthModule | Classe compilada em `AuthenticationManager.compiled` | Memória até restart/delete |
|
|
487
|
+
| Recompilação de AuthModule | `dates[id] <= module.updated` | Recompila e substitui o cache (`AuthRunner:48`) | Memória |
|
|
488
|
+
| Limpeza de cache | `add`/`delete` do módulo | `compiled.remove(id)`, `dates.remove(id)` | Memória |
|
|
489
|
+
| Reescrita de `username` | Auth Module chama `request.setParameter("username", ...)` | A senha/role passam a usar o novo username | Apenas na requisição |
|
|
490
|
+
| Refresh token rotativo | `grant_type=refresh_token` (form) | Novo refresh_token de 1 ano | Stateless (JWT) |
|
|
491
|
+
|
|
492
|
+
### 6.1 Cache e recompilação de Auth Module
|
|
493
|
+
|
|
494
|
+
```mermaid
|
|
495
|
+
flowchart TD
|
|
496
|
+
A[run module] --> B{module.updated == null?}
|
|
497
|
+
B -->|sim| C[add: persiste e define updated]
|
|
498
|
+
B -->|nao| D{classe em cache E dates[id] > updated?}
|
|
499
|
+
C --> D
|
|
500
|
+
D -->|sim| E[reutiliza classe compilada]
|
|
501
|
+
D -->|nao| F[getScript -> compile -> cacheia classe e data]
|
|
502
|
+
E --> G[executor: invoca authenticate]
|
|
503
|
+
F --> G
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### 6.2 Estrutura do script compilado (`AuthRunner.getScript:77`)
|
|
507
|
+
|
|
508
|
+
O `script` do módulo é embrulhado numa classe `FunifierScheduler` com imports pré-injetados (rede: `groovyx.net.http.*`, `com.mashape.unirest.http.Unirest`; entidades Funifier; `ManagerFactory`; `GameDao`) e a anotação `@TimedInterrupt(value = 5L, unit = SECONDS)` (`:141`). São injetados `setContext`, `setManager`, `setDatabase` e um `println(msg)` que **acumula em uma lista de output** (não imprime no console). No login o runner chama `obj.invokeMethod("authenticate", request)`.
|
|
509
|
+
|
|
510
|
+
### 6.3 Dois limites de tempo (não óbvio)
|
|
511
|
+
|
|
512
|
+
- `@TimedInterrupt(5L, SECONDS)` é **fixo em 5s** no script (`:141`) — interrompe em fronteiras de loop/método (proteção contra loops de CPU). **Ignora** `module.timeout`.
|
|
513
|
+
- `future.get(timeout, SECONDS)` usa `module.timeout` (default 5) (`AuthRunner:229-253`) — governa o tempo de parede total, inclusive chamadas HTTP bloqueantes.
|
|
514
|
+
|
|
515
|
+
Logo, aumentar `module.timeout` ajuda apenas chamadas externas bloqueantes; loops de CPU continuam limitados a 5s pelo `@TimedInterrupt`.
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## 7. Suportado vs NÃO Suportado
|
|
520
|
+
|
|
521
|
+
### ✅ Suportado
|
|
522
|
+
|
|
523
|
+
- OAuth 2.0 `grant_type=password` com Auth Modules (PAM stacking).
|
|
524
|
+
- OAuth 2.0 `grant_type=client_credentials`.
|
|
525
|
+
- OAuth 2.0 `grant_type=refresh_token` — **apenas form-urlencoded**.
|
|
526
|
+
- Auth Modules Groovy com `required`/`requisite`/`sufficient` (`optional` é aceito mas ignorado).
|
|
527
|
+
- Reescrita de `username` por Auth Module antes da validação.
|
|
528
|
+
- Auto-criação de jogador no 1º login (sem Principal).
|
|
529
|
+
- Escopo e whitelist por Application e por Role; merge de múltiplas roles.
|
|
530
|
+
- Role pública (Basic só com apiKey, sem secret) → escopo da role `public` (`SecurityFilter:153-160`).
|
|
531
|
+
- Refresh token rotativo de 1 ano.
|
|
532
|
+
|
|
533
|
+
### ❌ NÃO Suportado / limitações confirmadas
|
|
534
|
+
|
|
535
|
+
- **LDAP:** `LDAPAuthentication.java` está **inteiramente comentado** (corpo todo em comentário). O bloco `AUTH_MODE_LDAP` em `authenticate()` (`:483-502`) também está comentado. `LDAPConfig` (`active`, `host`, `port`, `dc`, `ou`) existe e é referenciado por `Security.ldap`, mas **não há autenticação LDAP funcional**.
|
|
536
|
+
- **Twitter OAuth:** campos `appIdTwitter`/`appSecretTwitter` existem em `Security`, mas não há implementação.
|
|
537
|
+
- **Facebook/Google OAuth:** `AuthenticationFacebook`/`AuthenticationGoogle` existem e são usados apenas pelo método **legado** `authenticate(auth_mode,...)`. Esse método **não tem chamadores em produção** (apenas o `main()` comentado do LDAP) e **não é exposto** por `/v3/auth/token`.
|
|
538
|
+
- **`grant_type=refresh_token` via JSON:** não implementado em `tokenJson`.
|
|
539
|
+
- **Modos `IMPLICIT`/`CREDENTIAL` no v3:** existem em `authenticate(auth_mode,...)` legado; não expostos em `/v3/auth/token` (o equivalente v3 de CREDENTIAL é `client_credentials`).
|
|
540
|
+
- **Revogação de JWT:** não há blacklist. O `logout` legado só apaga registros da coleção `authentication` (v1/v2). JWTs do `/v3/auth/token` não podem ser revogados antes da expiração.
|
|
541
|
+
- **2FA no login do jogador:** `TwoFactor`/`TwoFactorManager` existem, mas só são usados em `AccountRest` (usuários de conta/Studio), **não** no `/v3/auth/token` do jogador.
|
|
542
|
+
|
|
543
|
+
### Subsistemas correlatos — fora do escopo deste documento
|
|
544
|
+
|
|
545
|
+
- **`/v3/system/auth/module`** (`rest/v3/rest/system/AuthenticationRest.java`, `@Path("v3/system/auth")`): CRUD de auth modules de **usuário Studio**, usando `com.funifier.engine.system.auth.AuthenticationManager`. Tem checagem explícita `checkSystemPermission("api","/system/auth", op)`. Independente do `/v3/auth/module` do jogador.
|
|
546
|
+
- **`/v3/sso/saml/*`** (`SsoSamlRest.java`): SSO **SAML 2.0** que autentica **administradores Studio**, não jogadores. No callback (`/sp/assertion/{id}`), resolve um `AccountAdmin` e emite um **token de conta** via `generateAccountToken(account.id, user.id, "read_all, write_all, delete_all", DAY, 1)` (`SsoSamlRest.java:166`) — **não** um token de player. Path público (`SecurityFilter:113`). Configuração via `SsoSamlManager` (coleção `sso_saml`). **Bug:** o ramo `identityLocation == name_identifier` é morto — `if`/`else if` testam a mesma constante `IDENTITY_LOCATION_ATTRIBUTE_ELEMENT` (`SsoSamlRest.java:123-127`).
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
## 8. Segurança e Permissões
|
|
551
|
+
|
|
552
|
+
### 8.1 Chave secreta JWT hardcoded e compartilhada
|
|
553
|
+
|
|
554
|
+
`TokenUtil.java:25`:
|
|
555
|
+
```java
|
|
556
|
+
private static final String SECRET_KEY = "d61de14ad05f48c5283bad0f5924071d87766219";
|
|
557
|
+
```
|
|
558
|
+
Todos os JWTs (de todas as gamificações) são assinados com a **mesma** chave estática. A distinção de gamificação é o claim `apiKey` interno, **não** a assinatura. `getValueStudio` (header `Authorization: Studio ...`) também usa essa `SECRET_KEY` via auth0 `JWTVerifier` (`:183`).
|
|
559
|
+
|
|
560
|
+
> Subsistema separado: o **editor de widgets legado** `StudioRest` (`@Path("2.0.0/studio")`) usa uma chave diferente — `new JWTVerifier("@1983@#")` (`StudioRest.java:37`). Tokens desse editor **não** passam na validação `Studio` do `SecurityFilter` e vice-versa.
|
|
561
|
+
|
|
562
|
+
### 8.2 Injeção em query Mongo (`/v3/auth/module?q=`)
|
|
563
|
+
|
|
564
|
+
`AuthenticationManager.findAll:809`:
|
|
565
|
+
```java
|
|
566
|
+
if(q != null && q.trim().length() > 0) { query.append(", " + q); }
|
|
567
|
+
```
|
|
568
|
+
O parâmetro `q` é concatenado **cru** dentro da string da pipeline de agregação `$match`, sem sanitização. Superfície de injeção NoSQL para quem tiver escopo de leitura em `/v3/auth/module`.
|
|
569
|
+
|
|
570
|
+
### 8.3 Execução de código Groovy nos Auth Modules
|
|
571
|
+
|
|
572
|
+
A compilação usa `SecureASTCustomizer` com `TriggerExpressionChecker` (`AuthRunner.compile:183`). O checker **bloqueia**:
|
|
573
|
+
- Tipos: `System`, `ProcessBuilder`, `File`, `GroovyShell`, `GroovyObject` (`TriggerExpressionChecker:18-24`).
|
|
574
|
+
- Texto: `.execute`, `.getDB`, `.getMongo`, `.dropDatabase` (`:26-34`).
|
|
575
|
+
|
|
576
|
+
E há whitelist de tokens de operadores (aritméticos, comparação, lógicos, colchetes) (`:191-215`). **Não** bloqueia, porém, chamadas HTTP de saída — `Unirest` e `groovyx.net.http.*` são pré-importados (`getScript:80-105`). Portanto um Auth Module pode fazer requisições externas arbitrárias (timeout de 5s/`module.timeout`), mas **não** executar shell (`.execute`), acessar `File`/`ProcessBuilder`/`System` nem manipular o Mongo cru.
|
|
577
|
+
|
|
578
|
+
### 8.4 Bypass de escopo quando `scope == null`
|
|
579
|
+
|
|
580
|
+
No `SecurityFilter`, a verificação de escopo só roda se `scope != null` (`:200`). Os tipos `Account` e `Studio` **não** definem nem `scope` nem `whitelist` (`:170-177`); um Bearer sem claim `scope` também resulta em `scope==null`. Nesses casos **ambos os portões são pulados** — a etapa de escopo (`scope==null`) e a de whitelist (`whitelist==null`) — e a requisição é liberada após a validação do token. Ou seja, autenticação `Account`/`Studio` não sofre nenhuma restrição de escopo nem de IP/domínio.
|
|
581
|
+
|
|
582
|
+
Além disso, `POST /v3/mobile/device` é liberado **incondicionalmente** na verificação de escopo (`:238-242`), independentemente do escopo do token.
|
|
583
|
+
|
|
584
|
+
### 8.5 NPE no whitelist check
|
|
585
|
+
|
|
586
|
+
`isRequestWhiteListed` (`SecurityFilter:328`): se `referer` e `origin` forem ambos nulos, `url` permanece `null` e `url.startsWith(ref)` lança NPE — capturada pelo `catch` externo do `doFilter`, retornando **HTTP 401**.
|
|
587
|
+
|
|
588
|
+
### 8.6 Paths públicos (sem token) — lista real
|
|
589
|
+
|
|
590
|
+
Gerada de `SecurityFilter:107-150`:
|
|
591
|
+
`/customer`, `/v3/pub`, `/v3/version`, `/v3/player/password`, `/v3/system/user/password/code`, `/v3/auth/token`, `/v3/sso/saml`, `/v3/package/marketplace/`, `/v3/package/marketplace/aggregate`, `/v3/package/aggregate`, `/v3/system/template/request/aggregate`, `/v3/system/template/request_folder/aggregate`, `/v3/system/ai/knowledge`, `/v3/system/package/aggregate`, `/v3/system/country/aggregate`, `/v3/system/plan/aggregate`, `/v3/system/remote/ping`, `/v3/system/landing`, `/v3/system/blog`, `/v3/technique`, `/v3/coredrive`, `/v3/inline`, `/v3/crm/email/track/open`, `/v3/crm/email/oauth_callback`, `/v3/crm/calendar/oauth_callback`, `/v3/crm/doc/oauth_callback`, `/v3/crm/doc/link`, `/v3/ad/proxy-image`, `/v3/account/register`. Além de `GET` em `/v3/widget*`, `/v3/global*`, `/v3/system/global*`.
|
|
592
|
+
|
|
593
|
+
---
|
|
594
|
+
|
|
595
|
+
## 9. Observabilidade e Troubleshooting
|
|
596
|
+
|
|
597
|
+
### Diagnóstico rápido
|
|
84
598
|
|
|
85
599
|
```
|
|
86
|
-
|
|
600
|
+
# Emitir token de jogador
|
|
601
|
+
POST /v3/auth/token
|
|
602
|
+
Content-Type: application/json
|
|
603
|
+
{"grant_type":"password","apiKey":"<apiKey>","username":"<player>","password":"<senha>"}
|
|
604
|
+
|
|
605
|
+
# Listar auth modules na ordem de execução
|
|
606
|
+
GET /v3/auth/module
|
|
607
|
+
Authorization: Bearer <token>
|
|
608
|
+
|
|
609
|
+
# Inspecionar IP/origem como o servidor enxerga
|
|
610
|
+
GET /v3/auth/origin
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Queries MongoDB úteis
|
|
614
|
+
|
|
615
|
+
```javascript
|
|
616
|
+
// Auth modules na ordem real de execução
|
|
617
|
+
db.auth_module.find({}).sort({order: 1})
|
|
618
|
+
|
|
619
|
+
// Configuração de segurança da gamificação (documento único)
|
|
620
|
+
db.security.findOne({})
|
|
621
|
+
|
|
622
|
+
// Roles de um jogador específico
|
|
623
|
+
db.principal.findOne({_id: "<player_id>"})
|
|
624
|
+
|
|
625
|
+
// Sessões legadas v1/v2 (NÃO contém Bearer tokens v3)
|
|
626
|
+
db.authentication.find({}).sort({expires_in: -1}).limit(10)
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Erros comuns e causas (verificados no código)
|
|
630
|
+
|
|
631
|
+
| Origem | HTTP | Mensagem | Causa |
|
|
632
|
+
|---|---|---|---|
|
|
633
|
+
| `/v3/auth/token` cc | 401 | `unauthorized_client` | app_secret inválido/não cadastrado (ou duplicado — 5.5) |
|
|
634
|
+
| `/v3/auth/token` password | 400 | `password incorrect for player` | `requirePassword=true` + senha enviada e errada (`AuthenticationException` → `FunifierRuntimeException(BAD_REQUEST)`) |
|
|
635
|
+
| `/v3/auth/token` password | 400 | `player X does not exist...` | player inexistente e `createPlayerIfDontExist=false` |
|
|
636
|
+
| `/v3/auth/token` password | 401 | `auth_module_failed` (com `data`) | módulo retornou `status=2` **e** gerou `output` (println no script) |
|
|
637
|
+
| `/v3/auth/token` password | 400 | `invalid_grant` | `status=2` sem output, NPE de role nula (5.3), ou grant inválido |
|
|
638
|
+
| `SecurityFilter` | 401 | `Authorization token bearer is not valid.` | JWT expirado/malformado |
|
|
639
|
+
| `SecurityFilter` | 401 | `You dont have permission to {m} in {entity} ... you need {m}_{entity_full} or {m}_all` | escopo insuficiente |
|
|
640
|
+
| `SecurityFilter` | 401 | `You have exceeded your gamification daily api requests limits` | rate limit diário atingido |
|
|
641
|
+
| `SecurityFilter` | 401 | `Domain or IP is not authorized...` | requisição fora da whitelist |
|
|
642
|
+
|
|
643
|
+
> Observação: os erros de `password`/`player` carregam internamente status 401 (`AuthenticationException`), mas o REST os re-lança como `FunifierRuntimeException(e, BAD_REQUEST)` (`FunifierRuntimeException.java:21`), resultando em **HTTP 400**.
|
|
644
|
+
|
|
645
|
+
### O que verificar quando o login não funciona
|
|
646
|
+
|
|
647
|
+
1. A role `player` existe em `db.security.findOne({}).roles`? (Sem ela, players sem Principal causam o NPE de 5.3 → 400.)
|
|
648
|
+
2. `requirePassword`: se `true`, o player precisa ter senha BCrypt — e o campo `password` precisa ser enviado (5.1).
|
|
649
|
+
3. `createPlayerIfDontExist`: se `false`, o player deve existir antes.
|
|
650
|
+
4. Algum Auth Module `requisite` está retornando `null`? Liste `GET /v3/auth/module`.
|
|
651
|
+
5. Limite de players (`Account.players_allowed`) atingido?
|
|
652
|
+
6. O token foi emitido pelo endpoint **JSON**? Então não há whitelist nem refresh_token (2.4).
|
|
653
|
+
|
|
654
|
+
---
|
|
655
|
+
|
|
656
|
+
## 10. Exemplos Práticos
|
|
657
|
+
|
|
658
|
+
### Exemplo mínimo — login de jogador
|
|
659
|
+
|
|
660
|
+
```json
|
|
661
|
+
POST /v3/auth/token
|
|
662
|
+
Content-Type: application/json
|
|
663
|
+
|
|
664
|
+
{ "grant_type": "password", "apiKey": "5a3f...567", "username": "tom", "password": "123" }
|
|
665
|
+
```
|
|
666
|
+
> Com `requirePassword=false`, `password` pode ser qualquer valor — ou o campo pode ser omitido.
|
|
667
|
+
|
|
668
|
+
### Exemplo — server-to-server (client_credentials, com refresh)
|
|
669
|
+
|
|
87
670
|
```
|
|
671
|
+
POST /v3/auth/token
|
|
672
|
+
Content-Type: application/x-www-form-urlencoded
|
|
673
|
+
|
|
674
|
+
grant_type=client_credentials&client_id=5a3f...567&client_secret=meu-app-secret
|
|
675
|
+
```
|
|
676
|
+
O token usa o `scope` da Application; via form, retorna também `refresh_token`. Token válido por 7 dias.
|
|
677
|
+
|
|
678
|
+
### Exemplo avançado — Auth Module SSO externo (sufficient)
|
|
679
|
+
|
|
680
|
+
```groovy
|
|
681
|
+
// 'authenticate' deve retornar o id do jogador (String) ou null.
|
|
682
|
+
// 'manager' (ManagerFactory) e 'context' (AuthContext) já estão injetados.
|
|
683
|
+
String authenticate(request) {
|
|
684
|
+
String ssoToken = request.getHeader("X-SSO-Token")
|
|
685
|
+
if (ssoToken == null) return null
|
|
686
|
+
|
|
687
|
+
def resp = com.mashape.unirest.http.Unirest
|
|
688
|
+
.get("https://sso.empresa.com/validate")
|
|
689
|
+
.header("Authorization", "Bearer " + ssoToken)
|
|
690
|
+
.asString()
|
|
691
|
+
|
|
692
|
+
if (resp.getStatus() == 200) {
|
|
693
|
+
def body = new groovy.json.JsonSlurper().parseText(resp.getBody())
|
|
694
|
+
// opcional: normalizar o username repassado adiante
|
|
695
|
+
request.setParameter("username", body.userId)
|
|
696
|
+
return body.userId
|
|
697
|
+
}
|
|
698
|
+
return null
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
```json
|
|
702
|
+
POST /v3/auth/module
|
|
703
|
+
Authorization: Bearer <admin_token>
|
|
704
|
+
{ "_id": "sso_externo", "name": "SSO Empresa", "relevance": "sufficient", "order": 0, "timeout": 10, "script": "..." }
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
### Anti-pattern 1 — confiar na coleção `authentication` para validar Bearer v3
|
|
708
|
+
|
|
709
|
+
```
|
|
710
|
+
// NÃO FAÇA:
|
|
711
|
+
db.authentication.findOne({_id: "<bearer_token>"})
|
|
712
|
+
// O Bearer JWT v3 é stateless e NÃO é gravado em 'authentication' (legado v1/v2).
|
|
713
|
+
// Para validar, decodifique o JWT (HS512 + GZIP) com a SECRET_KEY.
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### Anti-pattern 2 — `requisite` que lança exceção esperando bloquear o login
|
|
717
|
+
|
|
718
|
+
```groovy
|
|
719
|
+
// NÃO FAÇA — exceção em 'requisite' é engolida (printStackTrace) e o loop continua:
|
|
720
|
+
String authenticate(request){ throw new RuntimeException("bloqueia!") } // NÃO bloqueia
|
|
721
|
+
// CORRETO para bloquear: 'requisite' deve RETORNAR null.
|
|
722
|
+
String authenticate(request){ if (!ok(request)) return null; return request.getParameter("username") }
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
### Anti-pattern 3 — emitir token de player via JSON e esperar whitelist de IP
|
|
726
|
+
|
|
727
|
+
```
|
|
728
|
+
// O endpoint JSON não inclui a claim 'whitelist' no token (2.4),
|
|
729
|
+
// então a restrição de IP/domínio da Role NÃO é aplicada pelo SecurityFilter.
|
|
730
|
+
// Use form-urlencoded quando a whitelist for relevante para segurança.
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
---
|
|
734
|
+
|
|
735
|
+
## Checklist de Configuração
|
|
736
|
+
|
|
737
|
+
- [ ] Role `player` existe em `security.roles` (sem ela, player sem Principal → NPE → 400 no login).
|
|
738
|
+
- [ ] `database` presente no scope da role se a gamificação usar `/v3/database`.
|
|
739
|
+
- [ ] `requirePassword` definido conforme a estratégia — lembrando que omitir o campo `password` contorna a checagem (5.1).
|
|
740
|
+
- [ ] `createPlayerIfDontExist` coerente com o cadastro de players (auto-criados não têm Principal).
|
|
741
|
+
- [ ] App registrado em `security.apps` (e `app_secret` **único** — duplicado quebra a autenticação, 5.5) para usar `client_credentials`.
|
|
742
|
+
- [ ] Auth Modules `requisite`/`sufficient` testados antes de produção — um script que lança exceção não bloqueia; para bloquear, retorne `null`.
|
|
743
|
+
- [ ] `module.timeout` ajustado para chamadas externas (o limite de loop continua fixo em 5s via `@TimedInterrupt`).
|
|
744
|
+
- [ ] Decisão consciente entre endpoint **form** (com refresh_token + whitelist) e **JSON** (sem ambos).
|
|
745
|
+
- [ ] `q` em `GET /v3/auth/module` nunca construído a partir de entrada não confiável (injeção NoSQL, 8.2).
|
|
88
746
|
|
|
89
|
-
|
|
747
|
+
---
|
|
90
748
|
|
|
91
|
-
|
|
92
|
-
2. Na seção **Roles**, clique no botão `+` para criar uma nova role
|
|
93
|
-
3. Defina: Role = `player`, Timeout = `1d`, Scope = `read_all, write_all, delete_all, database`
|
|
94
|
-
4. Clique em **Salvar**
|
|
95
|
-
5. O jogador precisa **fazer login novamente** para obter um token com as novas permissões
|
|
749
|
+
## REGRAS DE OURO
|
|
96
750
|
|
|
97
|
-
|
|
751
|
+
**NÃO faça:** inventar comportamento; assumir semântica REST/OAuth padrão sem confirmar no código; omitir inconsistências (NPE de role, bypass de escopo, injeção em `q`); simplificar o algoritmo PAM; ignorar código comentado (LDAP, campos legados).
|
|
98
752
|
|
|
99
|
-
|
|
100
|
-
- [ ] Token expira no tempo configurado
|
|
101
|
-
- [ ] Jogador inválido recebe erro apropriado
|
|
102
|
-
- [ ] SSO funciona (se configurado)
|
|
103
|
-
- [ ] Role "player" está configurada com scope adequado
|
|
104
|
-
- [ ] Token do jogador tem permissão para os endpoints necessários
|
|
753
|
+
**SEMPRE faça:** citar `arquivo:linha`; documentar campos removidos/ignorados silenciosamente; registrar a diferença entre o que o schema aceita e o que o runtime usa (claim `whitelist`, `optional` ignorado); indicar explicitamente o que **não** foi encontrado/implementado.
|