oscar_bridge_test.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. package handlers
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/json"
  6. "log/slog"
  7. "net/http"
  8. "net/http/httptest"
  9. "testing"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/mk6i/open-oscar-server/config"
  12. "github.com/mk6i/open-oscar-server/server/webapi/middleware"
  13. "github.com/mk6i/open-oscar-server/state"
  14. )
  15. // testListener is a listener group whose SSL half is present only when the
  16. // test asks for it.
  17. func testListener(sslAvailable bool) config.ListenerGroup {
  18. g := config.ListenerGroup{
  19. Name: "local",
  20. BOSListenAddress: "0.0.0.0:5190",
  21. BOSAdvertisedHostPlain: "bos.example.com:5190",
  22. }
  23. if sslAvailable {
  24. g.BOSListenAddressSSL = "0.0.0.0:5191"
  25. g.BOSAdvertisedHostSSL = "ssl.example.com:5193"
  26. }
  27. return g
  28. }
  29. // bridgeRequest builds a startOSCARSession request carrying the API key the
  30. // middleware would have put on the context.
  31. func bridgeRequest(query string, apiKey *state.WebAPIKey) *http.Request {
  32. req := httptest.NewRequest(http.MethodGet, "/aim/startOSCARSession?"+query, nil)
  33. if apiKey != nil {
  34. req = req.WithContext(context.WithValue(req.Context(), middleware.ContextKeyAPIKey, apiKey))
  35. }
  36. return req
  37. }
  38. // bridgeData is the data object of a successful startOSCARSession response.
  39. type bridgeData struct {
  40. Response struct {
  41. StatusCode int `json:"statusCode"`
  42. Data struct {
  43. Host string `json:"host"`
  44. Port int `json:"port"`
  45. Cookie string `json:"cookie"`
  46. TLSCertName string `json:"tlsCertName"`
  47. } `json:"data"`
  48. } `json:"response"`
  49. }
  50. func TestOSCARBridgeHandler_StartOSCARSession(t *testing.T) {
  51. validToken := base64.URLEncoding.EncodeToString(signedCookieFor("testuser"))
  52. unrestrictedKey := &state.WebAPIKey{DevID: "dev123"}
  53. tests := []struct {
  54. name string
  55. query string
  56. apiKey *state.WebAPIKey
  57. sslAvailable bool
  58. expectedCode int
  59. checkBody func(t *testing.T, body string)
  60. }{
  61. {
  62. // No tlsCertName, which is how the client reads "connect in the clear".
  63. name: "Success_Plaintext",
  64. query: "a=" + validToken,
  65. apiKey: unrestrictedKey,
  66. expectedCode: http.StatusOK,
  67. checkBody: func(t *testing.T, body string) {
  68. got := decodeBridgeData(t, body)
  69. assert.Equal(t, 200, got.Response.StatusCode)
  70. assert.Equal(t, "bos.example.com", got.Response.Data.Host)
  71. assert.Equal(t, 5190, got.Response.Data.Port)
  72. assert.Empty(t, got.Response.Data.TLSCertName)
  73. },
  74. },
  75. {
  76. name: "Success_TLS",
  77. query: "a=" + validToken + "&useTLS=1",
  78. apiKey: unrestrictedKey,
  79. sslAvailable: true,
  80. expectedCode: http.StatusOK,
  81. checkBody: func(t *testing.T, body string) {
  82. got := decodeBridgeData(t, body)
  83. assert.Equal(t, "ssl.example.com", got.Response.Data.Host)
  84. assert.Equal(t, 5193, got.Response.Data.Port)
  85. // The certificate is issued to the host the client is sent to.
  86. assert.Equal(t, "ssl.example.com", got.Response.Data.TLSCertName)
  87. },
  88. },
  89. {
  90. // Encryption the server cannot provide degrades to a plaintext host
  91. // rather than failing the handoff.
  92. name: "TLSRequestedButUnavailable_DegradesToPlaintext",
  93. query: "a=" + validToken + "&useTLS=true",
  94. apiKey: unrestrictedKey,
  95. sslAvailable: false,
  96. expectedCode: http.StatusOK,
  97. checkBody: func(t *testing.T, body string) {
  98. got := decodeBridgeData(t, body)
  99. assert.Equal(t, "bos.example.com", got.Response.Data.Host)
  100. assert.Empty(t, got.Response.Data.TLSCertName)
  101. },
  102. },
  103. {
  104. name: "Error_MissingToken",
  105. query: "",
  106. apiKey: unrestrictedKey,
  107. expectedCode: http.StatusUnauthorized,
  108. checkBody: func(t *testing.T, body string) {
  109. assert.Contains(t, body, "authentication token required")
  110. },
  111. },
  112. {
  113. name: "Error_TokenNotBase64",
  114. query: "a=not!valid!base64",
  115. apiKey: unrestrictedKey,
  116. expectedCode: http.StatusUnauthorized,
  117. checkBody: func(t *testing.T, body string) {
  118. assert.Contains(t, body, "invalid or expired token")
  119. },
  120. },
  121. {
  122. // A well-formed token the baker refuses to crack: wrong signature or
  123. // past its expiry.
  124. name: "Error_TokenFailsSignatureCheck",
  125. query: "a=" + base64.URLEncoding.EncodeToString([]byte("forged")),
  126. apiKey: unrestrictedKey,
  127. expectedCode: http.StatusUnauthorized,
  128. checkBody: func(t *testing.T, body string) {
  129. assert.Contains(t, body, "invalid or expired token")
  130. },
  131. },
  132. {
  133. name: "Error_NoAPIKeyOnContext",
  134. query: "a=" + validToken,
  135. apiKey: nil,
  136. expectedCode: http.StatusInternalServerError,
  137. checkBody: func(t *testing.T, body string) {
  138. assert.Contains(t, body, "internal server error")
  139. },
  140. },
  141. {
  142. name: "Error_APIKeyLacksBridgeCapability",
  143. query: "a=" + validToken,
  144. apiKey: &state.WebAPIKey{DevID: "dev123", Capabilities: []string{"presence"}},
  145. expectedCode: http.StatusForbidden,
  146. checkBody: func(t *testing.T, body string) {
  147. assert.Contains(t, body, "OSCAR bridge not enabled")
  148. },
  149. },
  150. {
  151. name: "Success_APIKeyGrantsBridgeCapability",
  152. query: "a=" + validToken,
  153. apiKey: &state.WebAPIKey{DevID: "dev123", Capabilities: []string{"presence", "oscar_bridge"}},
  154. expectedCode: http.StatusOK,
  155. checkBody: func(t *testing.T, body string) {
  156. assert.Equal(t, 200, decodeBridgeData(t, body).Response.StatusCode)
  157. },
  158. },
  159. }
  160. for _, tt := range tests {
  161. t.Run(tt.name, func(t *testing.T) {
  162. handler := &OSCARBridgeHandler{
  163. OSCARAuthService: &testAuthService{crackCookie: crackSignedCookie},
  164. Listener: testListener(tt.sslAvailable),
  165. Logger: slog.Default(),
  166. }
  167. rr := httptest.NewRecorder()
  168. handler.StartOSCARSession(rr, bridgeRequest(tt.query, tt.apiKey))
  169. assert.Equal(t, tt.expectedCode, rr.Code)
  170. tt.checkBody(t, rr.Body.String())
  171. })
  172. }
  173. }
  174. // The token arrives URL-safe, the way clientLogin minted it, and goes back out in
  175. // standard base64, the alphabet the client decodes the sign-on cookie with. The
  176. // cookie bytes here encode differently under each.
  177. func TestOSCARBridgeHandler_StartOSCARSession_ReencodesCookie(t *testing.T) {
  178. rawCookie := []byte{0xff, 0xef, 0xbe}
  179. urlSafe := base64.URLEncoding.EncodeToString(rawCookie)
  180. standard := base64.StdEncoding.EncodeToString(rawCookie)
  181. assert.NotEqual(t, urlSafe, standard, "test cookie must distinguish the two alphabets")
  182. var cracked []byte
  183. handler := &OSCARBridgeHandler{
  184. OSCARAuthService: &testAuthService{
  185. crackCookie: func(authCookie []byte) (state.ServerCookie, error) {
  186. cracked = authCookie
  187. return state.ServerCookie{ScreenName: "testuser"}, nil
  188. },
  189. },
  190. Listener: testListener(false),
  191. Logger: slog.Default(),
  192. }
  193. rr := httptest.NewRecorder()
  194. handler.StartOSCARSession(rr, bridgeRequest("a="+urlSafe, &state.WebAPIKey{DevID: "dev123"}))
  195. assert.Equal(t, http.StatusOK, rr.Code)
  196. assert.Equal(t, rawCookie, cracked, "the baker sees the decoded cookie")
  197. assert.Equal(t, standard, decodeBridgeData(t, rr.Body.String()).Response.Data.Cookie)
  198. }
  199. func decodeBridgeData(t *testing.T, body string) bridgeData {
  200. t.Helper()
  201. got := bridgeData{}
  202. assert.NoError(t, json.Unmarshal([]byte(body), &got))
  203. return got
  204. }