From 9f21bfc9ee3656bd53e7404b06c302627a5871bd Mon Sep 17 00:00:00 2001 From: Sebastian Sastre Date: Thu, 22 Feb 2024 22:07:31 -0300 Subject: [PATCH 1/4] fixes session cookie --- Ride-Builder/RideAppClassHelper.class.st | 23 +++- Ride/RidePresenter.class.st | 20 +++- Ride/RideService.class.st | 41 ++++++- Ride/RideSession.class.st | 137 +++++++++++++++++++++-- 4 files changed, 201 insertions(+), 20 deletions(-) diff --git a/Ride-Builder/RideAppClassHelper.class.st b/Ride-Builder/RideAppClassHelper.class.st index 2699a6b..d2a1d4d 100644 --- a/Ride-Builder/RideAppClassHelper.class.st +++ b/Ride-Builder/RideAppClassHelper.class.st @@ -53,7 +53,16 @@ RideAppClassHelper class >> newSessionWith_UrlMethodFor: aSymbol [ "Answer a new {1}App session with the given sessionId and url." - ^ {1}Session newWith: sessionId url: anUrl + ^ self sessionClass newWith: sessionId url: anUrl +' format: { aSymbol } +] + +{ #category : #accessing } +RideAppClassHelper class >> sessionClassMethodFor: aSymbol [ + + ^ 'sessionClass + + ^ {1}Session ' format: { aSymbol } ] @@ -90,11 +99,18 @@ RideAppClassHelper >> addInstallMethodTo: aClass for: aSymbol [ { #category : #actions } RideAppClassHelper >> addNewSessionWith_UrlMethodTo: aClass for: aSymbol [ - + self deprecated: 'not used'. aClass class compile: (self class newSessionWith_UrlMethodFor: aSymbol). aClass class organization classify: #'newSessionWith:url:' under: 'actions' ] +{ #category : #actions } +RideAppClassHelper >> addSessionClassMethodTo: aClass for: aSymbol [ + + aClass class compile: (self class sessionClassMethodFor: aSymbol). + aClass class organization classify: #sessionClass under: 'accessing' +] + { #category : #accessing } RideAppClassHelper >> classNameFor: aSymbol [ @@ -114,7 +130,8 @@ RideAppClassHelper >> for: aSymbol [ | appClass | appClass := super for: aSymbol. - self addNewSessionWith_UrlMethodTo: appClass for: aSymbol. + self addSessionClassMethodTo: appClass for: aSymbol. + "self addNewSessionWith_UrlMethodTo: appClass for: aSymbol." self addInstallMethodTo: appClass for: aSymbol. self addApplicationResourceClassMethodTo: appClass for: aSymbol. self addInitializeServerMethodTo: appClass for: aSymbol. diff --git a/Ride/RidePresenter.class.st b/Ride/RidePresenter.class.st index 1f6e5a2..29d6ce7 100644 --- a/Ride/RidePresenter.class.st +++ b/Ride/RidePresenter.class.st @@ -235,6 +235,15 @@ RidePresenter >> currentSession [ ^ RideCurrentSession value ] +{ #category : #actions } +RidePresenter >> ensureSessionCookie [ + + | response sessionCookie | + response := RideCurrentResponse value. + sessionCookie := self currentSession sessionCookie. + response headers at: 'Set-Cookie' put: sessionCookie fullString +] + { #category : #actions } RidePresenter >> formAction [ "Makes the receiver perform the action corresponding to a RESTful convention @@ -397,7 +406,7 @@ RidePresenter >> newAuthToken [ { #category : #actions } RidePresenter >> onAboutToRespond [ - "no-op is the default" + self ensureSessionCookie ] { #category : #actions } @@ -521,7 +530,7 @@ RidePresenter >> renderUsing: aSelector [ { #category : #actions } RidePresenter >> resetSession [ - Ride service invalidateSession: self currentSession + Ride service invalidateSessionId: self currentSession ] { #category : #accessing } @@ -577,6 +586,13 @@ RidePresenter >> templateContext [ template ] ] +{ #category : #accessing } +RidePresenter >> unauthorizedHandler [ + "Answers a handler to react when the auth token is missing or was not found." + + ^ [ RideUnauthorizedError signal: 'Authorization not found' ] +] + { #category : #actions } RidePresenter >> updateModelFromRequest: aRequest [ "Updates the model data following the convention of expecting for all its data keys, diff --git a/Ride/RideService.class.st b/Ride/RideService.class.st index 281145b..aa22c55 100644 --- a/Ride/RideService.class.st +++ b/Ride/RideService.class.st @@ -29,12 +29,12 @@ RideService class >> applicationResourceClass [ { #category : #accessing } RideService class >> getSessionGetter [ - ^ [ :request | + ^ [ :request | | sessionId | sessionId := self sessionIdFromCookieOrNewFrom: request. Ride service sessions at: sessionId - ifAbsentPut: [ self newSessionWith: sessionId url: request url ] ] + ifAbsentPut: [ self newSessionWith: sessionId request: request ] ] ] { #category : #accessing } @@ -61,9 +61,11 @@ RideService class >> log: aString level: aSymbol [ RideService class >> newResetSessionCookie [ | expires cookieString domain | + self deprecated: 'using this from the session'. domain := Ride service server router domain. expires := ZnUtils httpDate: DateAndTime now - 10 years. - cookieString := 'id={1}; expires={2}; path=/; domain={3}' format: { + cookieString := '{1}={2}; expires={3}; path=/; domain={4}' format: { + Ride service sessionClass cookieName. String new. expires. domain }. @@ -76,9 +78,15 @@ RideService class >> newSessionId [ ^ UUID new asString36 ] +{ #category : #actions } +RideService class >> newSessionWith: sessionId request: aRequest [ + + ^ self sessionClass newWith: sessionId request: aRequest +] + { #category : #accessing } RideService class >> newSessionWith: sessionId url: anUrl [ - + self deprecated: 'not used'. self subclassResponsibility ] @@ -98,13 +106,21 @@ RideService class >> restart [ ] { #category : #accessing } -RideService class >> sessionIdFromCookieOrNewFrom: aRequest [ +RideService class >> sessionClass [ + + ^ self subclassResponsibility +] +{ #category : #accessing } +RideService class >> sessionIdFromCookieOrNewFrom: aRequest [ "Answers the session ID taken from the cookie of the given aRequest. Answers a new one if the cookie is absent " + | cookieName | + cookieName := Ride service sessionClass cookieName. + ^ (aRequest cookies - detect: [ :each | each name = 'id' ] + detect: [ :each | each name = cookieName ] ifNone: [ ^ self newSessionId ]) value ] @@ -240,6 +256,13 @@ RideService >> invalidateSession: aRideSession [ put: Ride service class newResetSessionCookie fullString ] +{ #category : #actions } +RideService >> invalidateSessionId: aRideSessionId [ + "Remove the given session from the cache" + + sessions removeKey: aRideSessionId ifAbsent: [ ] +] + { #category : #accessing } RideService >> locator [ self deprecated: 'use Ride resource instead'. @@ -371,6 +394,12 @@ RideService >> server: anObject [ server := anObject ] +{ #category : #accessing } +RideService >> sessionClass [ + + ^ self class sessionClass +] + { #category : #accessing } RideService >> sessionGetter [ diff --git a/Ride/RideSession.class.st b/Ride/RideSession.class.st index d52e31b..163cfbb 100644 --- a/Ride/RideSession.class.st +++ b/Ride/RideSession.class.st @@ -9,23 +9,88 @@ Class { 'id', 'webSocket', 'cache', - 'values' + 'values', + 'authenticityTokens', + 'cookie' + ], + #classInstVars : [ + 'authenticityTokenGenerator' ], #category : #'Ride-Core' } +{ #category : #accessing } +RideSession class >> authenticityTokenGenerator [ + + ^ authenticityTokenGenerator := UUIDGenerator new +] + +{ #category : #actions } +RideSession class >> cookieName [ + "Answers the session cookie name." + + ^ self name asString +] + +{ #category : #actions } +RideSession class >> newSessionCookieId: anId expiring: aDateAndTime [ + + | domain expires cookieString | + domain := Ride service server router domain. + expires := ZnUtils httpDate: aDateAndTime. + cookieString := '{1}={2}; expires={3}; path=/; domain={4}' format: { + Ride service sessionClass cookieName. + anId. + expires. + domain }. + + ^ ZnCookie fromString: cookieString for: ('http://' , domain) asZnUrl +] + { #category : #'instance creation' } -RideSession class >> newWith: anId url: anUrl [ +RideSession class >> newWith: anId request: aRequest [ + + ^ self new initializeWith: anId request: aRequest +] +{ #category : #'instance creation' } +RideSession class >> newWith: anId url: anUrl [ + self deprecated: 'not used'. ^ self new initializeWith: anId url: anUrl ] { #category : #actions } -RideSession >> add: routeKey presenter: aRidePresenter [ +RideSession class >> nextAuthenticityTokenValue [ + + ^ self authenticityTokenGenerator next asString36 +] + +{ #category : #actions } +RideSession class >> reset [ + authenticityTokenGenerator := nil +] + +{ #category : #accessing } +RideSession class >> sessionDuration [ + + ^ (Smalltalk os environment + at: #SESSION_DURATION + ifAbsent: [ 20 years asSeconds ]) asInteger seconds +] + +{ #category : #actions } +RideSession >> add: routeKey presenter: aRidePresenter [ +self deprecated: ' not used'. ^ self presenters at: routeKey put: aRidePresenter ] +{ #category : #accessing } +RideSession >> authenticityTokens [ + + ^ authenticityTokens ifNil: [ self initializeAuthenticityTokens ] +] + { #category : #accessing } RideSession >> cache [ @@ -83,12 +148,34 @@ RideSession >> initialUrl: anUrl [ self values at: #initialUrl put: anUrl ] ] +{ #category : #initialization } +RideSession >> initializeAuthenticityTokens [ + + ^ authenticityTokens := TTLCache new +] + { #category : #initialization } RideSession >> initializeCache [ ^ cache := TTLCache new ] +{ #category : #initialization } +RideSession >> initializeCookieUsing: aRequest [ + + | newCookie | + + newCookie := [ + self class + newSessionCookieId: + self class nextAuthenticityTokenValue + expiring: DateAndTime now + self class sessionDuration ]. + + ^ cookie := aRequest cookies + detect: [ :e | e name = self class cookieName ] + ifNone: [ newCookie value ] +] + { #category : #initialization } RideSession >> initializeValues [ @@ -96,12 +183,26 @@ RideSession >> initializeValues [ ] { #category : #initialization } -RideSession >> initializeWith: anId url: anUrl [ +RideSession >> initializeWith: anId request: aRequest [ super initialize. id := anId. + self initializeCookieUsing: aRequest. self initializeCache. + self initializeAuthenticityTokens. + self initialUrl: aRequest url +] + +{ #category : #initialization } +RideSession >> initializeWith: anId url: anUrl [ + self deprecated: 'not in use'. + super initialize. + + id := anId. + self initializeCookie. + self initializeCache. + self initializeAuthenticityTokens. self initialUrl: anUrl ] @@ -109,10 +210,17 @@ RideSession >> initializeWith: anId url: anUrl [ RideSession >> invalidateAndRedirectTo: url [ | response | - Ride service invalidateSession: self. + Ride service invalidateSessionId: self id. response := RideCurrentResponse value. - "Redirect" + "Reset the session cookie" + response headers + at: 'Set-Cookie' + put: + (self class newSessionCookieExpiring: DateAndTime now - 10 years) + fullString. + + "and redirect" response code: 302; location: url. @@ -121,9 +229,14 @@ RideSession >> invalidateAndRedirectTo: url [ { #category : #actions } RideSession >> newAuthToken [ - "Adds a new auth token" - - ^ cache at: #authenticity_token put: self newUUIDAsString36 + "Adds a new auth token. + The authenticity tokens are stored by value with its own TTL + so request can be considered unauthorized when a request doesn't + include an existing one for this session." + + | value | + value := self class nextAuthenticityTokenValue. + ^ authenticityTokens at: value ifAbsentPut: [ value ] ] { #category : #actions } @@ -144,6 +257,12 @@ RideSession >> remove: routeKey [ ^ self presenters removeKey: routeKey ifAbsent: [ nil ] ] +{ #category : #accessing } +RideSession >> sessionCookie [ + + ^ cookie +] + { #category : #accessing } RideSession >> values [ From 07bda4f155af1b63a867240d3e0d6dd31d133b7b Mon Sep 17 00:00:00 2001 From: Sebastian Sastre Date: Fri, 23 Feb 2024 21:55:43 -0300 Subject: [PATCH 2/4] adds 401 makes TTLCaches thread safe --- Ride-Builder/RideAppHelper.class.st | 28 ++++++++++++++++++++ Ride/RidePresenter.class.st | 41 ++++++++++++++++++----------- Ride/RideService.class.st | 4 +-- Ride/RideSession.class.st | 4 +-- 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/Ride-Builder/RideAppHelper.class.st b/Ride-Builder/RideAppHelper.class.st index cf154d2..d129caf 100644 --- a/Ride-Builder/RideAppHelper.class.st +++ b/Ride-Builder/RideAppHelper.class.st @@ -175,6 +175,31 @@ RideAppHelper class >> fourZeroFourContentFor: aSymbol [ ' format: { aSymbol } ] +{ #category : #accessing } +RideAppHelper class >> fourZeroOneContentFor: aSymbol [ + + ^ ' + + + Unauthorized (401) + + + + +
+
+

Unauthorized

+

The authorization for the requested content was found to be invalid.

+
+

+ If you are the {1} application owner check the logs for more information. +

+
+ + +' format: { aSymbol } +] + { #category : #accessing } RideAppHelper class >> gitignoreContent [ @@ -581,6 +606,9 @@ RideAppHelper >> ensureTemplatesSharedOn: directory for: aSymbol [ directory ensureCreateDirectory. + directory / '401.html.stt' writeStreamDo: [ :stream | + stream nextPutAll: (self class fourZeroOneContentFor: aSymbol) ]. + directory / '404.html.stt' writeStreamDo: [ :stream | stream nextPutAll: (self class fourZeroFourContentFor: aSymbol) ]. diff --git a/Ride/RidePresenter.class.st b/Ride/RidePresenter.class.st index 29d6ce7..5a144d7 100644 --- a/Ride/RidePresenter.class.st +++ b/Ride/RidePresenter.class.st @@ -184,10 +184,17 @@ RidePresenter >> allowedFields [ "Answers the current request fields that are allowed to be used in this presenter." - | request allowed | - request := self currentRequest. + ^ self allowedFieldsFrom: self currentRequest +] + +{ #category : #accessing } +RidePresenter >> allowedFieldsFrom: aRequest [ + "Answers the given request fields that are allowed + to be used in this presenter." + + | allowed | allowed := self allowedParams. - ^ request entity fields associationsSelect: [ :field | + ^ aRequest entity fields associationsSelect: [ :field | allowed includes: field key ] ] @@ -251,8 +258,17 @@ RidePresenter >> formAction [ Signals an exception if no value is found for _method or when is found but it doesn't follow the REST convention." - | req restfulAction selector | + | req restfulAction selector unauthorized authenticityToken session | req := self currentRequest. + unauthorized := self unauthorizedHandler. + authenticityToken := req entity + at: #authenticity_token + ifAbsent: unauthorized. + session := self currentSession. + + (session authenticityTokens includesKey: authenticityToken) ifFalse: + unauthorized. + restfulAction := req entity at: #_method ifAbsent: [ @@ -264,12 +280,6 @@ RidePresenter >> formAction [ ^ self perform: selector ] -{ #category : #accessing } -RidePresenter >> getAllDataKeysOf: aMapless [ - - ^ aMapless maplessData keys reject: [ :e | e = '_c' or: [ e = 'id' ] ] -] - { #category : #accessing } RidePresenter >> getLayoutName [ @@ -598,10 +608,9 @@ RidePresenter >> updateModelFromRequest: aRequest [ "Updates the model data following the convention of expecting for all its data keys, parameters in the given request with their corresponding new value." - | allModelDataKeys | - allModelDataKeys := self getAllDataKeysOf: model. - allModelDataKeys do: [ :key | - | value | - value := aRequest entity at: key ifAbsent: [ nil ]. - value ifNotNil: [ model at: key put: value ] ] + | allDataFields | + allDataFields := self allowedFieldsFrom: aRequest. + + allDataFields keysAndValuesDo: [ :key :value | + model at: key put: value ] ] diff --git a/Ride/RideService.class.st b/Ride/RideService.class.st index aa22c55..d42c33a 100644 --- a/Ride/RideService.class.st +++ b/Ride/RideService.class.st @@ -190,7 +190,7 @@ RideService >> getSessionFrom: request [ { #category : #initialization } RideService >> initializeCache [ - ^ cache := TTLCache new + ^ cache := TTLCache new beThreadSafe ] { #category : #initialization } @@ -241,7 +241,7 @@ RideService >> initializeServer [ { #category : #initialization } RideService >> initializeSessions [ - ^ sessions := TTLCache new + ^ sessions := TTLCache new beThreadSafe ] { #category : #actions } diff --git a/Ride/RideSession.class.st b/Ride/RideSession.class.st index 163cfbb..b2b0f20 100644 --- a/Ride/RideSession.class.st +++ b/Ride/RideSession.class.st @@ -151,13 +151,13 @@ RideSession >> initialUrl: anUrl [ { #category : #initialization } RideSession >> initializeAuthenticityTokens [ - ^ authenticityTokens := TTLCache new + ^ authenticityTokens := TTLCache new beThreadSafe ] { #category : #initialization } RideSession >> initializeCache [ - ^ cache := TTLCache new + ^ cache := TTLCache new beThreadSafe ] { #category : #initialization } From 4fff0d029af95170e4aba5fc8c741e6d3d38780d Mon Sep 17 00:00:00 2001 From: Sebastian Sastre Date: Fri, 23 Feb 2024 22:07:15 -0300 Subject: [PATCH 3/4] added handler for RideUnauthorizedError --- Ride/RideServer.class.st | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/Ride/RideServer.class.st b/Ride/RideServer.class.st index 21e6dc0..4a8d31e 100644 --- a/Ride/RideServer.class.st +++ b/Ride/RideServer.class.st @@ -398,7 +398,7 @@ RideServer >> setExceptionHandlers [ "First handler is to prevent further processing in case of being under maintenance." self teapot - exception: RideMaintenanceException -> [ :ex :req | + exception: RideMaintenanceException -> [ :ex :req | response := TeaResponse ok. origin := req headers at: 'origin' ifAbsent: [ '*' ]. response body: self htmlMaintenancePage. @@ -408,10 +408,10 @@ RideServer >> setExceptionHandlers [ yourself. response ]; output: #html. - + "Second for redirecting to wherever the app said it should." self teapot - exception: RideRedirect -> [ :ex :req | + exception: RideRedirect -> [ :ex :req | response := TeaResponse redirect. response location: ex location. origin := req headers at: 'origin' ifAbsent: [ '*' ]. @@ -422,11 +422,11 @@ RideServer >> setExceptionHandlers [ yourself. response ]; output: #html. - + "Hanlder for any abort exception. It answers accordingly in HTTP. The process that signals this is expected to had loaded the current response with the content of this response." - self teapot exception: TeaAbort -> [ :abort :req | + self teapot exception: TeaAbort -> [ :abort :req | origin := req headers at: 'origin' ifAbsent: [ '*' ]. abort response headers @@ -438,7 +438,7 @@ RideServer >> setExceptionHandlers [ "Hanlder for validation issues reponds with an HTTP 400. If the signaler process wants to add details to the response it will have done that already by using the value of RideCurrentResponse." - self teapot exception: RideValidationError -> [ :ex :req | + self teapot exception: RideValidationError -> [ :ex :req | response := TeaResponse badRequest. origin := req headers at: 'origin' ifAbsent: [ '*' ]. response body: (ex describeOn: @@ -449,9 +449,13 @@ RideServer >> setExceptionHandlers [ yourself. response ]. + "Handler for request that are not properly authorized." + self teapot exception: + RideUnauthorizedError -> [ :ex :req | TeaResponse unauthorized ]. + "Hanlder for request that end up signaling an unprocessable exception. This one responds with HTTP 422 Unprocessable Content." - self teapot exception: RideUnprocessableEntity -> [ :ex :req | + self teapot exception: RideUnprocessableEntity -> [ :ex :req | response := TeaResponse code: 422. origin := req headers at: 'origin' ifAbsent: [ '*' ]. response body: (ex describeOn: @@ -464,7 +468,7 @@ RideServer >> setExceptionHandlers [ "Handle the not found exception to answer an HTTP 404" self teapot - exception: RideNotFoundError -> [ :ex :req | + exception: RideNotFoundError -> [ :ex :req | | templateContext | response := TeaResponse notFound. [ ex printString ] logError. @@ -472,7 +476,7 @@ RideServer >> setExceptionHandlers [ templateContext := sharedTemplates copy at: #status put: response code asString; yourself. - Ride isDevelopment ifTrue: [ + Ride isDevelopment ifTrue: [ templateContext at: #error put: { (#messageText -> ex messageText) } asDictionary ]. @@ -488,7 +492,7 @@ RideServer >> setExceptionHandlers [ "General Exception/Error handler hence anwering an HTTP 500" self teapot - exception: Error -> [ :ex :req | + exception: Error -> [ :ex :req | response := TeaResponse serverError. [ ex printString ] logError. origin := req headers at: 'origin' ifAbsent: [ '*' ]. From 684b1a5c7a8e761f236f3c68cbe2f3295db40b3e Mon Sep 17 00:00:00 2001 From: Sebastian Sastre Date: Sat, 24 Feb 2024 11:14:49 -0300 Subject: [PATCH 4/4] fixes main app presenter index template --- Ride-Builder/RideAppHomeTemplateHelper.class.st | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Ride-Builder/RideAppHomeTemplateHelper.class.st b/Ride-Builder/RideAppHomeTemplateHelper.class.st index 11a9d12..c19d9b7 100644 --- a/Ride-Builder/RideAppHomeTemplateHelper.class.st +++ b/Ride-Builder/RideAppHomeTemplateHelper.class.st @@ -11,9 +11,9 @@ Class { RideAppHomeTemplateHelper class >> indexHTMLTemplateFor: aSymbol [ ^ '
-

Welcome to

-

This page was rendered by a STTemplate generated for .

-

Find my source in

+

Welcome to <%= self webAppName %>

+

This page was rendered by a STTemplate generated for <%= self webAppName %>.

+

Find my source in <%= self templatePath %>

' format: { aSymbol } ]