oscar_bridge_test.go 7.0 KB

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