doorkeeper 5.1.2 → 5.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of doorkeeper might be problematic. Click here for more details.

Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +812 -0
  3. data/CONTRIBUTING.md +4 -9
  4. data/Dangerfile +1 -1
  5. data/Gemfile +2 -1
  6. data/NEWS.md +1 -819
  7. data/README.md +2 -2
  8. data/RELEASING.md +6 -5
  9. data/app/controllers/doorkeeper/applications_controller.rb +5 -3
  10. data/app/controllers/doorkeeper/authorized_applications_controller.rb +1 -1
  11. data/app/controllers/doorkeeper/tokens_controller.rb +18 -8
  12. data/app/validators/redirect_uri_validator.rb +19 -9
  13. data/app/views/doorkeeper/applications/_form.html.erb +0 -6
  14. data/app/views/doorkeeper/applications/show.html.erb +1 -1
  15. data/config/locales/en.yml +3 -1
  16. data/doorkeeper.gemspec +1 -1
  17. data/gemfiles/rails_5_0.gemfile +1 -0
  18. data/gemfiles/rails_5_1.gemfile +1 -0
  19. data/gemfiles/rails_5_2.gemfile +1 -0
  20. data/gemfiles/rails_6_0.gemfile +2 -1
  21. data/gemfiles/rails_master.gemfile +1 -0
  22. data/lib/doorkeeper.rb +3 -0
  23. data/lib/doorkeeper/config.rb +30 -3
  24. data/lib/doorkeeper/config/option.rb +13 -7
  25. data/lib/doorkeeper/grape/helpers.rb +5 -1
  26. data/lib/doorkeeper/helpers/controller.rb +16 -3
  27. data/lib/doorkeeper/oauth/authorization/code.rb +10 -8
  28. data/lib/doorkeeper/oauth/authorization/token.rb +1 -1
  29. data/lib/doorkeeper/oauth/code_response.rb +2 -2
  30. data/lib/doorkeeper/oauth/error_response.rb +1 -1
  31. data/lib/doorkeeper/oauth/helpers/uri_checker.rb +18 -4
  32. data/lib/doorkeeper/oauth/nonstandard.rb +39 -0
  33. data/lib/doorkeeper/oauth/refresh_token_request.rb +8 -8
  34. data/lib/doorkeeper/oauth/token_introspection.rb +13 -12
  35. data/lib/doorkeeper/orm/active_record.rb +17 -1
  36. data/lib/doorkeeper/orm/active_record/access_grant.rb +1 -1
  37. data/lib/doorkeeper/orm/active_record/access_token.rb +2 -2
  38. data/lib/doorkeeper/orm/active_record/application.rb +5 -65
  39. data/lib/doorkeeper/stale_records_cleaner.rb +6 -2
  40. data/lib/doorkeeper/version.rb +3 -3
  41. data/lib/generators/doorkeeper/previous_refresh_token_generator.rb +6 -6
  42. data/lib/generators/doorkeeper/templates/initializer.rb +41 -9
  43. data/lib/generators/doorkeeper/templates/migration.rb.erb +3 -0
  44. data/spec/controllers/applications_controller_spec.rb +93 -0
  45. data/spec/controllers/protected_resources_controller_spec.rb +3 -3
  46. data/spec/controllers/tokens_controller_spec.rb +71 -3
  47. data/spec/dummy/config/application.rb +3 -1
  48. data/spec/dummy/config/initializers/doorkeeper.rb +27 -9
  49. data/spec/lib/config_spec.rb +11 -0
  50. data/spec/lib/oauth/helpers/uri_checker_spec.rb +17 -2
  51. data/spec/lib/oauth/pre_authorization_spec.rb +0 -15
  52. data/spec/models/doorkeeper/application_spec.rb +268 -373
  53. data/spec/requests/flows/authorization_code_spec.rb +16 -4
  54. data/spec/requests/flows/revoke_token_spec.rb +19 -11
  55. data/spec/support/doorkeeper_rspec.rb +1 -1
  56. data/spec/validators/redirect_uri_validator_spec.rb +39 -14
  57. metadata +7 -15
  58. data/.coveralls.yml +0 -1
  59. data/.github/ISSUE_TEMPLATE.md +0 -25
  60. data/.github/PULL_REQUEST_TEMPLATE.md +0 -17
  61. data/.gitignore +0 -20
  62. data/.gitlab-ci.yml +0 -16
  63. data/.hound.yml +0 -3
  64. data/.rspec +0 -1
  65. data/.rubocop.yml +0 -50
  66. data/.travis.yml +0 -35
@@ -28,8 +28,8 @@ feature "Authorization Code Flow" do
28
28
  config_is_set(:token_secret_strategy, ::Doorkeeper::SecretStoring::Sha256Hash)
29
29
  end
30
30
 
31
- scenario "Authorization Code Flow with hashing" do
32
- @client.redirect_uri = Doorkeeper.configuration.native_redirect_uri
31
+ def authorize(redirect_url)
32
+ @client.redirect_uri = redirect_url
33
33
  @client.save!
34
34
  visit authorization_endpoint_url(client: @client)
35
35
  click_on "Authorize"
@@ -42,16 +42,28 @@ feature "Authorization Code Flow" do
42
42
  hashed_code = Doorkeeper::AccessGrant.secret_strategy.transform_secret code
43
43
  expect(hashed_code).to eq Doorkeeper::AccessGrant.first.token
44
44
 
45
+ [code, hashed_code]
46
+ end
47
+
48
+ scenario "using redirect_url urn:ietf:wg:oauth:2.0:oob" do
49
+ code, hashed_code = authorize("urn:ietf:wg:oauth:2.0:oob")
45
50
  expect(code).not_to eq(hashed_code)
51
+ i_should_see "Authorization code:"
52
+ i_should_see code
53
+ i_should_not_see hashed_code
54
+ end
46
55
 
56
+ scenario "using redirect_url urn:ietf:wg:oauth:2.0:oob:auto" do
57
+ code, hashed_code = authorize("urn:ietf:wg:oauth:2.0:oob:auto")
58
+ expect(code).not_to eq(hashed_code)
47
59
  i_should_see "Authorization code:"
48
60
  i_should_see code
49
61
  i_should_not_see hashed_code
50
62
  end
51
63
  end
52
64
 
53
- scenario "resource owner authorizes using test url" do
54
- @client.redirect_uri = Doorkeeper.configuration.native_redirect_uri
65
+ scenario "resource owner authorizes using oob url" do
66
+ @client.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
55
67
  @client.save!
56
68
  visit authorization_endpoint_url(client: @client)
57
69
  click_on "Authorize"
@@ -40,16 +40,14 @@ describe "Revoke Token Flow" do
40
40
  end
41
41
 
42
42
  context "with invalid token to revoke" do
43
- it "should not revoke any tokens and respond successfully" do
43
+ it "should not revoke any tokens and respond with forbidden" do
44
44
  expect do
45
45
  post revocation_token_endpoint_url,
46
46
  params: { token: "I_AM_AN_INVALID_TOKEN" },
47
47
  headers: headers
48
48
  end.not_to(change { Doorkeeper::AccessToken.where(revoked_at: nil).count })
49
49
 
50
- # The authorization server responds with HTTP status code 200 even if
51
- # token is invalid
52
- expect(response).to be_successful
50
+ expect(response).to be_forbidden
53
51
  end
54
52
  end
55
53
 
@@ -59,19 +57,23 @@ describe "Revoke Token Flow" do
59
57
  credentials = Base64.encode64("#{client_id}:poop")
60
58
  { "HTTP_AUTHORIZATION" => "Basic #{credentials}" }
61
59
  end
62
- it "should not revoke any tokens and respond successfully" do
60
+ it "should not revoke any tokens and respond with forbidden" do
63
61
  post revocation_token_endpoint_url, params: { token: access_token.token }, headers: headers
64
62
 
65
- expect(response).to be_successful
63
+ expect(response).to be_forbidden
64
+ expect(response.body).to include("unauthorized_client")
65
+ expect(response.body).to include(I18n.t("doorkeeper.errors.messages.revoke.unauthorized"))
66
66
  expect(access_token.reload.revoked?).to be_falsey
67
67
  end
68
68
  end
69
69
 
70
70
  context "with no credentials and a valid token" do
71
- it "should not revoke any tokens and respond successfully" do
71
+ it "should not revoke any tokens and respond with forbidden" do
72
72
  post revocation_token_endpoint_url, params: { token: access_token.token }
73
73
 
74
- expect(response).to be_successful
74
+ expect(response).to be_forbidden
75
+ expect(response.body).to include("unauthorized_client")
76
+ expect(response.body).to include(I18n.t("doorkeeper.errors.messages.revoke.unauthorized"))
75
77
  expect(access_token.reload.revoked?).to be_falsey
76
78
  end
77
79
  end
@@ -88,7 +90,9 @@ describe "Revoke Token Flow" do
88
90
  it "should not revoke the token as its unauthorized" do
89
91
  post revocation_token_endpoint_url, params: { token: access_token.token }, headers: headers
90
92
 
91
- expect(response).to be_successful
93
+ expect(response).to be_forbidden
94
+ expect(response.body).to include("unauthorized_client")
95
+ expect(response.body).to include(I18n.t("doorkeeper.errors.messages.revoke.unauthorized"))
92
96
  expect(access_token.reload.revoked?).to be_falsey
93
97
  end
94
98
  end
@@ -127,14 +131,18 @@ describe "Revoke Token Flow" do
127
131
  it "should not revoke the access token provided" do
128
132
  post revocation_token_endpoint_url, params: { token: access_token.token }
129
133
 
130
- expect(response).to be_successful
134
+ expect(response).to be_forbidden
135
+ expect(response.body).to include("unauthorized_client")
136
+ expect(response.body).to include(I18n.t("doorkeeper.errors.messages.revoke.unauthorized"))
131
137
  expect(access_token.reload.revoked?).to be_falsey
132
138
  end
133
139
 
134
140
  it "should not revoke the refresh token provided" do
135
141
  post revocation_token_endpoint_url, params: { token: access_token.token }
136
142
 
137
- expect(response).to be_successful
143
+ expect(response).to be_forbidden
144
+ expect(response.body).to include("unauthorized_client")
145
+ expect(response.body).to include(I18n.t("doorkeeper.errors.messages.revoke.unauthorized"))
138
146
  expect(access_token.reload.revoked?).to be_falsey
139
147
  end
140
148
  end
@@ -16,7 +16,7 @@ module Doorkeeper
16
16
  # Tries to find ORM from the Gemfile used to run test suite
17
17
  def self.detect_orm
18
18
  orm = (ENV["BUNDLE_GEMFILE"] || "").match(/Gemfile\.(.+)\.rb/)
19
- (orm && orm[1] || :active_record).to_sym
19
+ (orm && orm[1] || ENV["ORM"] || :active_record).to_sym
20
20
  end
21
21
  end
22
22
  end
@@ -18,7 +18,7 @@ describe RedirectUriValidator do
18
18
  #
19
19
  # @see https://www.oauth.com/oauth2-servers/redirect-uris/redirect-uris-native-apps/
20
20
  it "is valid when the uri is custom native URI" do
21
- subject.redirect_uri = "myapp://callback"
21
+ subject.redirect_uri = "myapp:/callback"
22
22
  expect(subject).to be_valid
23
23
  end
24
24
 
@@ -27,33 +27,48 @@ describe RedirectUriValidator do
27
27
  expect(subject).to be_valid
28
28
  end
29
29
 
30
- it "accepts native redirect uri" do
30
+ it "accepts nonstandard oob redirect uri" do
31
31
  subject.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
32
32
  expect(subject).to be_valid
33
33
  end
34
34
 
35
- it "rejects if test uri is disabled" do
36
- allow(RedirectUriValidator).to receive(:native_redirect_uri).and_return(nil)
37
- subject.redirect_uri = "urn:some:test"
38
- expect(subject).not_to be_valid
35
+ it "accepts nonstandard oob:auto redirect uri" do
36
+ subject.redirect_uri = "urn:ietf:wg:oauth:2.0:oob:auto"
37
+ expect(subject).to be_valid
39
38
  end
40
39
 
41
40
  it "is invalid when the uri is not a uri" do
42
41
  subject.redirect_uri = "]"
43
42
  expect(subject).not_to be_valid
44
- expect(subject.errors[:redirect_uri].first).to eq("must be a valid URI.")
43
+ expect(subject.errors[:redirect_uri].first).to eq(I18n.t("activerecord.errors.models.doorkeeper/application.attributes.redirect_uri.invalid_uri"))
45
44
  end
46
45
 
47
46
  it "is invalid when the uri is relative" do
48
47
  subject.redirect_uri = "/abcd"
49
48
  expect(subject).not_to be_valid
50
- expect(subject.errors[:redirect_uri].first).to eq("must be an absolute URI.")
49
+ expect(subject.errors[:redirect_uri].first).to eq(I18n.t("activerecord.errors.models.doorkeeper/application.attributes.redirect_uri.relative_uri"))
51
50
  end
52
51
 
53
52
  it "is invalid when the uri has a fragment" do
54
53
  subject.redirect_uri = "https://example.com/abcd#xyz"
55
54
  expect(subject).not_to be_valid
56
- expect(subject.errors[:redirect_uri].first).to eq("cannot contain a fragment.")
55
+ expect(subject.errors[:redirect_uri].first).to eq(I18n.t("activerecord.errors.models.doorkeeper/application.attributes.redirect_uri.fragment_present"))
56
+ end
57
+
58
+ it "is invalid when scheme resolves to localhost (needs an explict scheme)" do
59
+ subject.redirect_uri = "localhost:80"
60
+ expect(subject).to be_invalid
61
+ expect(subject.errors[:redirect_uri].first).to eq(I18n.t("activerecord.errors.models.doorkeeper/application.attributes.redirect_uri.unspecified_scheme"))
62
+ end
63
+
64
+ it "is invalid if an ip address" do
65
+ subject.redirect_uri = "127.0.0.1:8080"
66
+ expect(subject).to be_invalid
67
+ end
68
+
69
+ it "accepts an ip address based URI if a scheme is specified" do
70
+ subject.redirect_uri = "https://127.0.0.1:8080"
71
+ expect(subject).to be_valid
57
72
  end
58
73
 
59
74
  context "force secured uri" do
@@ -62,13 +77,23 @@ describe RedirectUriValidator do
62
77
  expect(subject).to be_valid
63
78
  end
64
79
 
65
- it "accepts native redirect uri" do
66
- subject.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
80
+ it "accepts custom scheme redirect uri (as per rfc8252 section 7.1)" do
81
+ subject.redirect_uri = "com.example.app:/oauth/callback"
82
+ expect(subject).to be_valid
83
+ end
84
+
85
+ it "accepts custom scheme redirect uri (as per rfc8252 section 7.1) #2" do
86
+ subject.redirect_uri = "com.example.app:/test"
87
+ expect(subject).to be_valid
88
+ end
89
+
90
+ it "accepts custom scheme redirect uri (common misconfiguration we have decided to allow)" do
91
+ subject.redirect_uri = "com.example.app://oauth/callback"
67
92
  expect(subject).to be_valid
68
93
  end
69
94
 
70
- it "accepts app redirect uri" do
71
- subject.redirect_uri = "some-awesome-app://oauth/callback"
95
+ it "accepts custom scheme redirect uri (common misconfiguration we have decided to allow) #2" do
96
+ subject.redirect_uri = "com.example.app://test"
72
97
  expect(subject).to be_valid
73
98
  end
74
99
 
@@ -118,7 +143,7 @@ describe RedirectUriValidator do
118
143
  subject.redirect_uri = "http://example.com/callback"
119
144
  expect(subject).not_to be_valid
120
145
  error = subject.errors[:redirect_uri].first
121
- expect(error).to eq("must be an HTTPS/SSL URI.")
146
+ expect(error).to eq(I18n.t("activerecord.errors.models.doorkeeper/application.attributes.redirect_uri.secured_uri"))
122
147
  end
123
148
  end
124
149
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doorkeeper
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.1.2
4
+ version: 5.2.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Felipe Elias Philipp
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2020-10-19 00:00:00.000000000 Z
14
+ date: 2019-05-23 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: railties
@@ -174,16 +174,8 @@ executables: []
174
174
  extensions: []
175
175
  extra_rdoc_files: []
176
176
  files:
177
- - ".coveralls.yml"
178
- - ".github/ISSUE_TEMPLATE.md"
179
- - ".github/PULL_REQUEST_TEMPLATE.md"
180
- - ".gitignore"
181
- - ".gitlab-ci.yml"
182
- - ".hound.yml"
183
- - ".rspec"
184
- - ".rubocop.yml"
185
- - ".travis.yml"
186
177
  - Appraisals
178
+ - CHANGELOG.md
187
179
  - CODE_OF_CONDUCT.md
188
180
  - CONTRIBUTING.md
189
181
  - Dangerfile
@@ -269,6 +261,7 @@ files:
269
261
  - lib/doorkeeper/oauth/helpers/unique_token.rb
270
262
  - lib/doorkeeper/oauth/helpers/uri_checker.rb
271
263
  - lib/doorkeeper/oauth/invalid_token_response.rb
264
+ - lib/doorkeeper/oauth/nonstandard.rb
272
265
  - lib/doorkeeper/oauth/password_access_token_request.rb
273
266
  - lib/doorkeeper/oauth/pre_authorization.rb
274
267
  - lib/doorkeeper/oauth/refresh_token_request.rb
@@ -471,12 +464,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
471
464
  version: '2.4'
472
465
  required_rubygems_version: !ruby/object:Gem::Requirement
473
466
  requirements:
474
- - - ">="
467
+ - - ">"
475
468
  - !ruby/object:Gem::Version
476
- version: '0'
469
+ version: 1.3.1
477
470
  requirements: []
478
- rubyforge_project:
479
- rubygems_version: 2.7.9
471
+ rubygems_version: 3.0.2
480
472
  signing_key:
481
473
  specification_version: 4
482
474
  summary: OAuth 2 provider for Rails and Grape
@@ -1 +0,0 @@
1
- service_name: travis-ci
@@ -1,25 +0,0 @@
1
- ### Steps to reproduce
2
- What we need to do to see your problem or bug?
3
-
4
- The more detailed the issue, the more likely that we will fix it ASAP.
5
-
6
- Don't use GitHub issues for questions like "How can I do that?" —
7
- use [StackOverflow](https://stackoverflow.com/questions/tagged/doorkeeper)
8
- instead with the corresponding tag.
9
-
10
- ### Expected behavior
11
- Tell us what should happen
12
-
13
- ### Actual behavior
14
- Tell us what happens instead
15
-
16
- ### System configuration
17
- You can help us to understand your problem if you will share some very
18
- useful information about your project environment (don't forget to
19
- remove any confidential data if it exists).
20
-
21
- **Doorkeeper initializer**:
22
-
23
- **Ruby version**:
24
-
25
- **Gemfile.lock**:
@@ -1,17 +0,0 @@
1
- ### Summary
2
-
3
- Provide a general description of the code changes in your pull
4
- request... were there any bugs you had fixed? If so, mention them. If
5
- these bugs have open GitHub issues, be sure to tag them here as well,
6
- to keep the conversation linked together.
7
-
8
- ### Other Information
9
-
10
- If there's anything else that's important and relevant to your pull
11
- request, mention that information here. This could include
12
- benchmarks, or other information.
13
-
14
- If you are updating NEWS.md file or are asked to update it by reviewers,
15
- please add the changelog entry at the top of the file.
16
-
17
- Thanks for contributing to Doorkeeper project!
data/.gitignore DELETED
@@ -1,20 +0,0 @@
1
- .bundle/
2
- .rbx
3
- *.rbc
4
- log/*.log
5
- pkg/
6
- spec/dummy/db/*.sqlite3
7
- spec/dummy/log/*.log
8
- spec/dummy/tmp/
9
- spec/generators/tmp
10
- Gemfile.lock
11
- gemfiles/*.lock
12
- .rvmrc
13
- *.swp
14
- .idea
15
- /.yardoc/
16
- /_yardoc/
17
- /doc/
18
- /rdoc/
19
- coverage
20
- *.gem
@@ -1,16 +0,0 @@
1
- dependency_scanning:
2
- image: docker:stable
3
- variables:
4
- DOCKER_DRIVER: overlay2
5
- allow_failure: true
6
- services:
7
- - docker:stable-dind
8
- script:
9
- - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
10
- - docker run
11
- --env DEP_SCAN_DISABLE_REMOTE_CHECKS="${DEP_SCAN_DISABLE_REMOTE_CHECKS:-false}"
12
- --volume "$PWD:/code"
13
- --volume /var/run/docker.sock:/var/run/docker.sock
14
- "registry.gitlab.com/gitlab-org/security-products/dependency-scanning:$SP_VERSION" /code
15
- artifacts:
16
- paths: [gl-dependency-scanning-report.json]
data/.hound.yml DELETED
@@ -1,3 +0,0 @@
1
- rubocop:
2
- config_file: .rubocop.yml
3
- version: 0.64.0
data/.rspec DELETED
@@ -1 +0,0 @@
1
- --colour
@@ -1,50 +0,0 @@
1
- AllCops:
2
- TargetRubyVersion: 2.4
3
- Exclude:
4
- - "spec/dummy/db/*"
5
- - "spec/dummy/config/*"
6
- - "Dangerfile"
7
- - "gemfiles/*.gemfile"
8
-
9
- Metrics/BlockLength:
10
- Exclude:
11
- - spec/**/*
12
- - lib/doorkeeper/rake/*
13
-
14
- Metrics/LineLength:
15
- Exclude:
16
- - spec/**/*
17
- Max: 100
18
-
19
- Metrics/MethodLength:
20
- Exclude:
21
- - spec/dummy/db/*
22
-
23
- Style/StringLiterals:
24
- EnforcedStyle: double_quotes
25
- Style/StringLiteralsInInterpolation:
26
- EnforcedStyle: double_quotes
27
-
28
- Style/FrozenStringLiteralComment:
29
- Enabled: true
30
-
31
- Style/TrailingCommaInHashLiteral:
32
- EnforcedStyleForMultiline: consistent_comma
33
- Style/TrailingCommaInArrayLiteral:
34
- EnforcedStyleForMultiline: consistent_comma
35
-
36
- Style/SymbolArray:
37
- MinSize: 3
38
- Style/WordArray:
39
- MinSize: 3
40
-
41
- Style/ClassAndModuleChildren:
42
- Exclude:
43
- - spec/**/*
44
-
45
- Layout/MultilineMethodCallIndentation:
46
- EnforcedStyle: indented
47
- Layout/TrailingBlankLines:
48
- Enabled: true
49
- Layout/DotPosition:
50
- EnforcedStyle: leading
@@ -1,35 +0,0 @@
1
- language: ruby
2
- cache: bundler
3
-
4
- rvm:
5
- - 2.4
6
- - 2.5
7
- - 2.6
8
- - ruby-head
9
-
10
- #before_install:
11
- # - gem update --system
12
- # - gem install bundler
13
-
14
- gemfile:
15
- - gemfiles/rails_5_0.gemfile
16
- - gemfiles/rails_5_1.gemfile
17
- - gemfiles/rails_5_2.gemfile
18
- - gemfiles/rails_6_0.gemfile
19
- - gemfiles/rails_master.gemfile
20
-
21
- matrix:
22
- fast_finish: true
23
- # Run Danger only once
24
- include:
25
- - rvm: 2.5
26
- gemfile: gemfiles/rails_5_2.gemfile
27
- script: bundle exec danger
28
- exclude:
29
- - gemfile: gemfiles/rails_6_0.gemfile
30
- rvm: 2.4
31
- - gemfile: gemfiles/rails_master.gemfile
32
- rvm: 2.4
33
- allow_failures:
34
- - gemfile: gemfiles/rails_master.gemfile
35
- - rvm: ruby-head