Skip to content

Commit 402c1e4

Browse files
committed
Use OAuth Device flow
Signed-off-by: dusan <borovcanindusan1@gmail.com>
1 parent 0cfd36a commit 402c1e4

File tree

13 files changed

+1104
-102
lines changed

13 files changed

+1104
-102
lines changed

cmd/users/main.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
grpcDomainsV1 "github.com/absmach/supermq/api/grpc/domains/v1"
2020
grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1"
2121
grpcUsersV1 "github.com/absmach/supermq/api/grpc/users/v1"
22+
redisclient "github.com/absmach/supermq/internal/clients/redis"
2223
"github.com/absmach/supermq/internal/email"
2324
smqlog "github.com/absmach/supermq/logger"
2425
smqauthn "github.com/absmach/supermq/pkg/authn"
@@ -97,6 +98,7 @@ type config struct {
9798
PasswordResetEmailTemplate string `env:"SMQ_PASSWORD_RESET_EMAIL_TEMPLATE" envDefault:"reset-password-email.tmpl"`
9899
VerificationURLPrefix string `env:"SMQ_VERIFICATION_URL_PREFIX" envDefault:"http://localhost/verify-email"`
99100
VerificationEmailTemplate string `env:"SMQ_VERIFICATION_EMAIL_TEMPLATE" envDefault:"verification-email.tmpl"`
101+
CacheURL string `env:"SMQ_USERS_CACHE_URL" envDefault:"redis://localhost:6379/0"`
100102
PassRegex *regexp.Regexp
101103
}
102104

@@ -152,6 +154,13 @@ func main() {
152154
exitCode = 1
153155
return
154156
}
157+
cacheClient, err := redisclient.Connect(cfg.CacheURL)
158+
if err != nil {
159+
logger.Error(fmt.Sprintf("failed to connect to redis: %s", err))
160+
exitCode = 1
161+
return
162+
}
163+
defer cacheClient.Close()
155164

156165
migration := postgres.Migration()
157166
db, err := pgclient.Setup(dbConfig, *migration)
@@ -275,7 +284,7 @@ func main() {
275284

276285
mux := chi.NewRouter()
277286
idp := uuid.New()
278-
httpSrv := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(csvc, authnMiddleware, tokenClient, cfg.SelfRegister, mux, logger, cfg.InstanceID, cfg.PassRegex, idp, oauthProvider), logger)
287+
httpSrv := httpserver.NewServer(ctx, cancel, svcName, httpServerConfig, httpapi.MakeHandler(csvc, authnMiddleware, tokenClient, cfg.SelfRegister, mux, logger, cfg.InstanceID, cfg.PassRegex, idp, cacheClient, oauthProvider), logger)
279288

280289
if cfg.SendTelemetry {
281290
chc := chclient.New(svcName, supermq.Version, logger, cancel)

docker/.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ SMQ_PASSWORD_RESET_URL_PREFIX=http://localhost/password-reset
269269
SMQ_PASSWORD_RESET_EMAIL_TEMPLATE=reset-password-email.tmpl
270270
SMQ_VERIFICATION_URL_PREFIX=http://localhost/verify-email
271271
SMQ_VERIFICATION_EMAIL_TEMPLATE=verification-email.tmpl
272+
SMQ_USERS_CACHE_URL=redis://users-redis:${SMQ_REDIS_TCP_PORT}/0
272273

273274
#### Users Client Config
274275
SMQ_USERS_URL=http://users:9002

docker/docker-compose.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ networks:
1010

1111
volumes:
1212
supermq-users-db-volume:
13+
supermq-users-redis-volume:
1314
supermq-groups-db-volume:
1415
supermq-clients-db-volume:
1516
supermq-channels-db-volume:
@@ -823,11 +824,21 @@ services:
823824
volumes:
824825
- supermq-users-db-volume:/var/lib/postgresql/data
825826

827+
users-redis:
828+
image: docker.io/redis:8.2.2-alpine3.22
829+
container_name: supermq-users-redis
830+
restart: on-failure
831+
networks:
832+
- supermq-base-net
833+
volumes:
834+
- supermq-users-redis-volume:/data
835+
826836
users:
827837
image: docker.io/supermq/users:${SMQ_RELEASE_TAG}
828838
container_name: supermq-users
829839
depends_on:
830840
- users-db
841+
- users-redis
831842
- auth
832843
- nats
833844
restart: on-failure
@@ -864,6 +875,7 @@ services:
864875
SMQ_USERS_DB_SSL_CERT: ${SMQ_USERS_DB_SSL_CERT}
865876
SMQ_USERS_DB_SSL_KEY: ${SMQ_USERS_DB_SSL_KEY}
866877
SMQ_USERS_DB_SSL_ROOT_CERT: ${SMQ_USERS_DB_SSL_ROOT_CERT}
878+
SMQ_USERS_CACHE_URL: ${SMQ_USERS_CACHE_URL}
867879
SMQ_USERS_ALLOW_SELF_REGISTER: ${SMQ_USERS_ALLOW_SELF_REGISTER}
868880
SMQ_EMAIL_HOST: ${SMQ_EMAIL_HOST}
869881
SMQ_EMAIL_PORT: ${SMQ_EMAIL_PORT}

users/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ The service is configured using the environment variables presented in the follo
5757
| `SMQ_JAEGER_TRACE_RATIO` | Jaeger sampling ratio | 1.0 |
5858
| `SMQ_SEND_TELEMETRY` | Send telemetry to supermq call home server. | true |
5959
| `SMQ_USERS_INSTANCE_ID` | SuperMQ instance ID | "" |
60+
| `SMQ_USERS_CACHE_URL` | Cache database URL | redis://localhost:6379/0 |
6061

6162
## Deployment
6263

@@ -120,6 +121,7 @@ SMQ_OAUTH_UI_ERROR_URL=http://localhost:9095/error \
120121
SMQ_USERS_DELETE_INTERVAL=24h \
121122
SMQ_USERS_DELETE_AFTER=720h \
122123
SMQ_USERS_INSTANCE_ID="" \
124+
SMQ_USERS_CACHE_URL=redis://localhost:6379/0 \
123125
$GOBIN/supermq-users
124126
```
125127

users/api/endpoint_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
usersapi "github.com/absmach/supermq/users/api"
3030
"github.com/absmach/supermq/users/mocks"
3131
"github.com/go-chi/chi/v5"
32+
"github.com/redis/go-redis/v9"
3233
"github.com/stretchr/testify/assert"
3334
"github.com/stretchr/testify/mock"
3435
)
@@ -97,7 +98,9 @@ func newUsersServer() (*httptest.Server, *mocks.Service, *authnmocks.Authenticat
9798
authn := new(authnmocks.Authentication)
9899
am := smqauthn.NewAuthNMiddleware(authn)
99100
token := new(authmocks.TokenServiceClient)
100-
usersapi.MakeHandler(svc, am, token, true, mux, logger, "", passRegex, idp, provider)
101+
// Create a mock Redis client for testing (won't be used in these tests)
102+
redisClient := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
103+
usersapi.MakeHandler(svc, am, token, true, mux, logger, "", passRegex, idp, redisClient, provider)
101104

102105
return httptest.NewServer(mux), svc, authn
103106
}

users/api/oauth.go

Lines changed: 4 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -257,23 +257,23 @@ func handleDeviceFlowCallback(w http.ResponseWriter, r *http.Request, oauth oaut
257257
deviceCode, err := deviceStore.GetByUserCode(userCode)
258258
if err != nil {
259259
w.Header().Set("Content-Type", "text/html")
260-
fmt.Fprint(w, `<html><body><h1>Invalid Code</h1><p>The device code is invalid or has expired.</p></body></html>`)
260+
fmt.Fprint(w, strings.Replace(errorHTML, "{{ERROR_MESSAGE}}", "The device code is invalid or has expired.", 1))
261261
return
262262
}
263263

264264
// Get OAuth authorization code
265265
code := r.FormValue("code")
266266
if code == "" {
267267
w.Header().Set("Content-Type", "text/html")
268-
fmt.Fprint(w, `<html><body><h1>Error</h1><p>No authorization code received.</p></body></html>`)
268+
fmt.Fprint(w, strings.Replace(errorHTML, "{{ERROR_MESSAGE}}", "No authorization code received.", 1))
269269
return
270270
}
271271

272272
// Exchange OAuth code for token
273273
token, err := oauth.Exchange(r.Context(), code)
274274
if err != nil {
275275
w.Header().Set("Content-Type", "text/html")
276-
fmt.Fprintf(w, `<html><body><h1>Error</h1><p>Failed to exchange code: %s</p></body></html>`, err.Error())
276+
fmt.Fprint(w, strings.Replace(errorHTML, "{{ERROR_MESSAGE}}", fmt.Sprintf("Failed to exchange code: %s.", err.Error()), 1))
277277
return
278278
}
279279

@@ -282,87 +282,11 @@ func handleDeviceFlowCallback(w http.ResponseWriter, r *http.Request, oauth oaut
282282
deviceCode.AccessToken = token.AccessToken
283283
if err := deviceStore.Update(deviceCode); err != nil {
284284
w.Header().Set("Content-Type", "text/html")
285-
fmt.Fprintf(w, `<html><body><h1>Error</h1><p>Failed to approve device: %s</p></body></html>`, err.Error())
285+
fmt.Fprint(w, strings.Replace(errorHTML, "{{ERROR_MESSAGE}}", fmt.Sprintf("Failed to approve device: %s.", err.Error()), 1))
286286
return
287287
}
288288

289289
// Show success page
290290
w.Header().Set("Content-Type", "text/html")
291-
successHTML := `<!DOCTYPE html>
292-
<html lang="en">
293-
<head>
294-
<meta charset="UTF-8">
295-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
296-
<title>Device Approved - Magistrala</title>
297-
<style>
298-
* { margin: 0; padding: 0; box-sizing: border-box; }
299-
body {
300-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
301-
background: #073764;
302-
min-height: 100vh;
303-
display: flex;
304-
align-items: center;
305-
justify-content: center;
306-
padding: 20px;
307-
}
308-
.container {
309-
background: white;
310-
border-radius: 16px;
311-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
312-
padding: 48px;
313-
max-width: 500px;
314-
width: 100%;
315-
text-align: center;
316-
}
317-
.success-icon {
318-
width: 80px;
319-
height: 80px;
320-
margin: 0 auto 24px;
321-
background: #10b981;
322-
border-radius: 50%;
323-
display: flex;
324-
align-items: center;
325-
justify-content: center;
326-
}
327-
.checkmark {
328-
width: 50px;
329-
height: 50px;
330-
}
331-
.checkmark-path {
332-
stroke: white;
333-
stroke-width: 4;
334-
fill: none;
335-
stroke-linecap: round;
336-
stroke-linejoin: round;
337-
}
338-
h1 {
339-
color: #073764;
340-
font-size: 32px;
341-
margin-bottom: 16px;
342-
}
343-
p {
344-
color: #4a5568;
345-
font-size: 18px;
346-
line-height: 1.6;
347-
}
348-
@media (max-width: 600px) {
349-
.container { padding: 32px 24px; }
350-
h1 { font-size: 28px; }
351-
}
352-
</style>
353-
</head>
354-
<body>
355-
<div class="container">
356-
<div class="success-icon">
357-
<svg class="checkmark" viewBox="0 0 52 52">
358-
<path class="checkmark-path" d="M14 27l10 10 18-20"/>
359-
</svg>
360-
</div>
361-
<h1>Device Approved!</h1>
362-
<p>Your device has been successfully authorized.</p>
363-
<p>You can now close this window and return to your device.</p>
364-
</div>
365-
</body>
366-
</html>`
367291
fmt.Fprint(w, successHTML)
368292
}

users/api/oauth_device.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,13 @@ type DeviceCode struct {
4040
VerificationURI string `json:"verification_uri"`
4141
ExpiresIn int `json:"expires_in"`
4242
Interval int `json:"interval"`
43-
Provider string `json:"-"`
44-
CreatedAt time.Time `json:"-"`
45-
State string `json:"-"`
46-
AccessToken string `json:"-"`
47-
Approved bool `json:"-"`
48-
Denied bool `json:"-"`
49-
LastPoll time.Time `json:"-"`
43+
Provider string `json:"provider,omitempty"`
44+
CreatedAt time.Time `json:"created_at,omitempty"`
45+
State string `json:"state,omitempty"`
46+
AccessToken string `json:"access_token,omitempty"`
47+
Approved bool `json:"approved,omitempty"`
48+
Denied bool `json:"denied,omitempty"`
49+
LastPoll time.Time `json:"last_poll,omitempty"`
5050
}
5151

5252
// DeviceCodeStore manages device authorization codes.

0 commit comments

Comments
 (0)